Nikita Mandrykin

Пишу код — от битов до бинов.

Назад

FPU в NASM: Вещественные числа, Синусы и Точность

📚 Содержание

🤔 1. Что такое FPU простыми словами?

Представьте калькулятор внутри процессора. Обычный процессор умеет работать только с целыми числами:

  • ✅ Сложить: 5 + 3 = 8
  • ✅ Умножить: 10 × 2 = 20
  • ❌ Но разделить 10 ÷ 3 = 3.333... — не может! Получится 3 (дробная часть потеряна)

FPU (Floating Point Unit) — это специальный “калькулятор для дробных чисел” внутри процессора, который умеет:

  • Работать с числами типа 3.14159, 0.0001, 1.5e-10
  • Вычислять синусы, косинусы, логарифмы
  • Сохранять очень большие ($10^{308}$) и очень маленькие ($10^{-308}$) числа

Словарь: основные термины

Термин Расшифровка Что означает
FPU Floating Point Unit Блок вещественных вычислений
float — Вещественное число (дробное)
ST(0) Stack Top Вершина стека FPU (самый верхний элемент)
fld Floating Load Загрузить число в FPU
fstp Floating Store and Pop Сохранить число и выгрузить из FPU
dword Double Word 4 байта = 32 бита (тип float в C)
qword Quad Word 8 байт = 64 бита (тип double в C)
tword Ten bytes 10 байт = 80 бит (максимальная точность FPU)

🚀 2. Быстрый старт

Вот минимальный пример, который умножает число 3.14 на 2:

section .data
    pi dq 3.14159265358979    ; dq = qword = 8 байт = double
    two dq 2.0
    result dq 0.0

section .text
global _start

_start:
    fld qword [pi]         ; Шаг 1: загрузить π в FPU
    fld qword [two]        ; Шаг 2: загрузить 2 в FPU
    fmulp                  ; Шаг 3: умножить и выгрузить одно число
    fstp qword [result]    ; Шаг 4: сохранить результат в память
    
    ; Выход из программы
    mov rax, 60
    xor rdi, rdi
    syscall

Разбор по шагам

Шаг 1: fld qword [pi] — что происходит?

fld = Floating LoaD (загрузить вещественное число)

  • qword говорит: “это 8-байтовое число типа double”
  • [pi] — адрес в памяти, где лежит число 3.14159…
  • Результат: число помещается на вершину стека FPU
    Стек FPU после fld [pi]:
    ┌────────────┐
    │ 3.14159... │ ← ST(0) (вершина)
    └────────────┘
Шаг 2: fld qword [two] — почему снова fld?

Потому что умножение требует ДВА числа! Загружаем второе:

Стек FPU после fld [two]:
┌────────────┐
│ 2.0        │ ← ST(0) (новая вершина!)
├────────────┤
│ 3.14159... │ ← ST(1) (π "провалилось" вниз)
└────────────┘

Важно: Каждый новый fld кладёт число СВЕРХУ, старые элементы сдвигаются вниз.

Шаг 3: fmulp — что такое суффикс "p"?

fmulp = Floating MULtiply and Pop

  • Умножает два верхних числа: ST(0) × ST(1)
  • Суффикс p означает pop (выгрузить) — убирает один элемент
    До fmulp:              После fmulp:
    ┌────────────┐         ┌────────────┐
    │ 2.0        │ ST(0)   │ 6.28318... │ ← ST(0) (результат 3.14×2)
    ├────────────┤         └────────────┘
    │ 3.14159... │ ST(1)
    └────────────┘
    (два элемента)         (один элемент)
Шаг 4: fstp qword [result] — сохранение результата

fstp = Floating STore and Pop

  • st = сохранить (store) в память
  • p = выгрузить (pop) из стека
  • qword = сохранить как 8-байтовое число
    До fstp:               После fstp:
    ┌────────────┐         ┌────────────┐
    │ 6.28318... │         │  (пусто)   │
    └────────────┘         └────────────┘
    
    Память [result] = 6.28318...

✅ Теперь стек FPU пуст и готов к новым вычислениям!


Словарь суффиксов инструкций

В FPU суффиксы после команды меняют её поведение:

Суффикс Что означает Пример Действие
p Pop (выгрузить) faddp Выполнить операцию + убрать один элемент
r Reverse (обратный порядок) fsubr Поменять местами операнды
i Integer (целое число) fild Загрузить/сохранить как целое
Нет суффикса Базовая операция fadd Операция без дополнительных действий

Примеры:

  • fadd — сложить два числа, оставить оба на стеке
  • faddp — сложить два числа, убрать одно (с pop)
  • fiadd — сложить с целым числом (с integer)

3. Зачем это нужно?

Проблема целочисленной арифметики

Работая только с целыми числами (int, long), вы сталкиваетесь с ограничениями:

❌ Целые числа (int, long)

Что можно:

  • Точные вычисления с целыми: 42 + 17 = 59 ✅
  • Быстрые операции (сложение за 1 такт)
  • Деление с остатком: 10 ÷ 3 = 3 (остаток 1)

Чего НЕЛЬЗЯ:

  • Представить $\frac{1}{3}$ точно (получится 0!)
  • Вычислить $\sqrt{2}$ (нет инструкции)
  • Работать с числами больше $2^{63}$
  • Тригонометрия, логарифмы — невозможны

Пример проблемы:

; Попытка разделить 10 на 3
mov rax, 10
mov rbx, 3
xor rdx, rdx
div rbx
; ❌ rax = 3 (дробь .333... потеряна!)
; rdx = 1 (остаток)

Вывод:

Для научных расчётов, 3D графики, физики — целых чисел недостаточно.

✅ Вещественные числа (FPU)

Что появляется:

  • Дробные числа: 3.14159, 0.001, 1.5e-10 ✅
  • Огромный диапазон: от $10^{-308}$ до $10^{308}$
  • Встроенная математика: sin, cos, log, sqrt
  • Стандарт IEEE 754 (как в C/C++/Python)

Те же задачи решаются:

  • $\frac{1}{3} = 0.333333…$ (точное представление)
  • $\sqrt{2} = 1.414213…$ (есть инструкция fsqrt)
  • Можно работать с $10^{100}$ и $10^{-100}$
  • fsin, fcos — встроены в процессор!

Тот же пример:

; Деление 10 на 3 с FPU
fild dword [ten]    ; 10.0
fild dword [three]  ; 3.0
fdivp               ; 10.0 / 3.0
; ✅ ST(0) = 3.333333... (точно!)

Вывод:

FPU открывает мир научных вычислений.


Реальные сценарии использования

Физика, химия, биология — везде нужны вещественные числа:

Пример: вычисление кинетической энергии

$$E_k = \frac{mv^2}{2}$$

section .data
    mass dq 5.0      ; кг
    velocity dq 10.0 ; м/с
    two dq 2.0
    energy dq 0.0

section .text
    fld qword [mass]      ; m
    fld qword [velocity]  ; v
    fmul st0, st0         ; v²
    fmulp                 ; m × v²
    fld qword [two]       ; 2
    fdivp                 ; (m × v²) / 2
    fstp qword [energy]   ; E = 250 Дж

Повороты, масштабирование, перспектива — всё на вещественных числах:

Пример: нормализация вектора

Нормализация = приведение длины вектора к 1, сохраняя направление.

$$\vec{v}_{norm} = \frac{\vec{v}}{|\vec{v}|} = \frac{\vec{v}}{\sqrt{x^2 + y^2 + z^2}}$$

; Вычислить длину вектора (x, y, z)
fld qword [x]
fmul st0, st0      ; x²
fld qword [y]
fmul st0, st0      ; y²
faddp              ; x² + y²
fld qword [z]
fmul st0, st0      ; z²
faddp              ; x² + y² + z²
fsqrt              ; √(x² + y² + z²) = длина
; Теперь можно разделить каждую компоненту на длину

Пример: сложный процент

$$A = P \times (1 + r)^n$$

Мы преобразуем формулу для FPU: $$A = P \times 2^{(n \cdot \log_2(1+r))}$$

section .data
    principal dq 1000.0   ; Начальная сумма 1000$
    rate dq 0.05          ; 5% годовых
    years dq 10.0         ; 10 лет
    result dq 0.0

section .text
    ; 1. Подготовим основание (1 + r)
    fld1                  ; ST0 = 1.0
    fld qword [rate]      ; ST0 = 0.05, ST1 = 1.0
    faddp                 ; ST0 = 1.05 (Base)

    ; 2. Загрузим степень (years)
    fld qword [years]     ; ST0 = 10.0, ST1 = 1.05

    ; 3. Вычислим показатель степени для двойки: years * log2(Base)
    ; fyl2x вычисляет: ST1 * log2(ST0) и выталкивает ST0
    ; Нам нужно: 10.0 * log2(1.05). Сейчас 10.0 в ST0.
    fxch                  ; Меняем местами: ST0 = 1.05, ST1 = 10.0
    fyl2x                 ; ST0 = 10.0 * log2(1.05) ≈ 0.701

    ; 4. Возведение 2 в степень ST0 (Power = 2^ST0)
    ; f2xm1 работает только если -1.0 < ST0 < 1.0.
    ; Поэтому разбиваем число на целую и дробную части: 2^(I+F) = 2^I * 2^F
    fld st0               ; Дублируем значение: ST0=0.701, ST1=0.701
    frndint               ; Округляем до целого: ST0=1.0 (или 0.0)
    fsubr st1, st0        ; Вычитаем целое из исх: ST0 = дробная часть (F)
                          ; ST1 = целая часть (I)
    
    f2xm1                 ; ST0 = (2^F) - 1
    fld1                  ; ST0 = 1.0
    faddp                 ; ST0 = 2^F
    
    fscale                ; ST0 = ST0 * 2^(ST1(целое)) = 2^F * 2^I
    fstp st1              ; Убираем целую часть (I) из стека, она больше не нужна
                          ; Теперь ST0 = (1.05)^10 ≈ 1.62889

    ; 5. Умножаем на начальную сумму
    fld qword [principal] ; ST0 = 1000.0, ST1 = 1.62889
    fmulp                 ; ST0 = 1628.89

    fstp qword [result]   ; Сохраняем итог в память

📚 4. Стек FPU: как это устроено?

Критическое ограничение: только 8 регистров!

FPU имеет всего 8 регистров стека: ST(0) до ST(7)

Это означает:

  • ✅ Можете загрузить максимум 8 чисел
  • ❌ 9-я операция fld вызовет переполнение стека и ошибку
  • 🔑 Поэтому обязательно используйте команды с p (pop) — они освобождают место!
❌ Без pop — стек переполнится
fld qword [a]    ; ST(0) занят (1/8)
fld qword [b]    ; ST(0-1) заняты (2/8)
fadd             ; ST(0-1) заняты (2/8)
; ❌ Результат остался в стеке!

fld qword [c]    ; ST(0-2) заняты (3/8)
fld qword [d]    ; ST(0-3) заняты (4/8)
fmul             ; ST(0-3) заняты (4/8)
; ❌ Ещё результат!

; После 6-7 таких операций:
; ❌ ПЕРЕПОЛНЕНИЕ! Программа упадёт

Проблема:

Без p результаты накапливаются в стеке, и через 8 операций места не останется.

✅ С pop — стек чист
fld qword [a]    ; ST(0) занят (1/8)
fld qword [b]    ; ST(0-1) заняты (2/8)
faddp            ; ST(0) занят (1/8) ← pop освободил!
fstp qword [r1]  ; Стек пуст (0/8) ← сохранили

fld qword [c]    ; ST(0) занят (1/8)
fld qword [d]    ; ST(0-1) заняты (2/8)
fmulp            ; ST(0) занят (1/8) ← pop освободил!
fstp qword [r2]  ; Стек пуст (0/8) ← сохранили

; ✅ Можем повторять бесконечно!

Решение:

Команды с p (faddp, fmulp, fstp) выталкивают элементы, освобождая место.


Почему именно p (pop)?

p = Pop = “Вытолкнуть из стека”

Когда вы делаете faddp:

  1. Складываются ST(0) и ST(1)
  2. Результат помещается в ST(0)
  3. Один элемент удаляется (pop!) — стек уменьшается на 1

Без p (команда fadd):

  1. Складываются ST(0) и ST(1)
  2. Результат помещается в ST(0)
  3. ST(1) остаётся! — стек не уменьшается
Пример: faddp (с pop)           Пример: fadd (без pop)

До операции:                    До операции:
┌───────┐                        ┌───────┐
│  3.0  │ ST(0)                  │  3.0  │ ST(0)
├───────┤                        ├───────┤
│  5.0  │ ST(1)                  │  5.0  │ ST(1)
├───────┤                        ├───────┤
│  7.0  │ ST(2)                  │  7.0  │ ST(2)
└───────┘                        └───────┘

После faddp:                    После fadd:
┌───────┐                        ┌───────┐
│  8.0  │ ST(0) ← 3+5            │  8.0  │ ST(0) ← 3+5
├───────┤                        ├───────┤
│  7.0  │ ST(1) ← было ST(2)     │  5.0  │ ST(1) ← осталось!
└───────┘                        ├───────┤
                                 │  7.0  │ ST(2)
Стек: 2 элемента                 └───────┘
                                 Стек: 3 элемента

Вывод: Используйте p после операций, чтобы стек не рос!


Главное отличие от обычных регистров

Обычные регистры процессора

RAX, RBX, RCX, RDX…

┌──────┐ ┌──────┐ ┌──────┐
│ RAX  │ │ RBX  │ │ RCX  │
│  42  │ │  17  │ │ 100  │
└──────┘ └──────┘ └──────┘

Принцип работы:

  • Как полка с ящиками
  • Каждый ящик имеет имя
  • Можете взять любой в любой момент
  • Нет ограничения на количество операций

Доступ:

mov rax, 42    ; Записать в RAX
mov rbx, rax   ; Скопировать из RAX в RBX
add rcx, rax   ; Сложить RAX и RCX
; Можем делать бесконечно!
Стек FPU (ТОЛЬКО 8!)

ST(0), ST(1), ST(2)… ST(7)

┌──────────┐
│  ST(0)   │ ← Вершина
├──────────┤
│  ST(1)   │
├──────────┤
│  ST(2)   │
├──────────┤
│   ...    │
├──────────┤
│  ST(7)   │ ← Дно (последний!)
└──────────┘

Принцип работы:

  • Как стопка из 8 тарелок
  • Можете взять только ВЕРХНЮЮ
  • Максимум 8 элементов одновременно!
  • Нужно освобождать место через p

Доступ:

fld qword [x]   ; +1 элемент в стеке
fadd            ; Операция (элементов столько же)
fstp qword [y]  ; -1 элемент (освободили!)

⚙️ 5. Базовые инструкции FPU

1. Загрузка чисел

Загрузка из памяти
; fld = Floating Load (загрузить)

fld dword [float32]    ; 32-бит (float)
fld qword [float64]    ; 64-бит (double)
fld tword [float80]    ; 80-бит (extended)

; Все они КЛАДУТ число на вершину стека
Загрузка констант
; Специальные команды для констант

fld1        ; Загрузить 1.0
fldz        ; Загрузить 0.0
fldpi       ; Загрузить π (3.14159...)
fldl2e      ; Загрузить log₂(e) ≈ 1.442695
fldl2t      ; Загрузить log₂(10) ≈ 3.321928

Обратите внимание на букву i — она означает Integer!

fild — загрузить и конвертировать
; fild = Floating Integer Load
; Загружает целое и конвертирует в float

section .data
    age dd 42          ; Целое 32-бит

section .text
    fild dword [age]   ; Загрузит как 42.0
    ; ST(0) = 42.0 (теперь это float!)
Разница fld vs fild
section .data
    x_float dd 42.0    ; Уже float
    x_int dd 42        ; Целое

section .text
    ; ПРАВИЛЬНО:
    fld dword [x_float]   ; ✅ Загрузить float
    fild dword [x_int]    ; ✅ Загрузить int → float
    
    ; НЕПРАВИЛЬНО:
    fld dword [x_int]     ; ❌ Попытка прочитать
                          ;    int как float!

2. Сохранение чисел

fstp — сохранить и выгрузить
; fstp = Floating STore and Pop
; Сохраняет ST(0) в память и УБИРАЕТ из стека

fstp dword [result32]   ; → float (32 бит)
fstp qword [result64]   ; → double (64 бит)
fstp tword [result80]   ; → extended (80 бит)

; После fstp элемент исчезает из стека
fst — сохранить без выгрузки
; fst = Floating STore (без Pop!)
; Сохраняет ST(0), но ОСТАВЛЯЕТ в стеке

fst qword [backup]     ; Скопировать ST(0)
; ST(0) всё ещё на месте!

; Полезно для промежуточных результатов:
fld qword [x]
fmul st0, st0          ; x²
fst qword [x_squared]  ; Сохранить x²
fsqrt                  ; √(x²) = x
; Теперь есть и x², и x
fistp — сохранить как целое
; fistp = Floating Integer STore and Pop
; Конвертирует float → int и сохраняет

section .data
    result dd 0

section .text
    fld qword [pi]         ; ST(0) = 3.14159...
    fistp dword [result]   ; [result] = 3
    
    ; Дробная часть отбрасывается
    ; (с округлением, см. режимы FPU)
Режимы округления
; По умолчанию: округление к ближайшему

fld qword [value]      ; 42.7
fistp dword [res]      ; res = 43 (ближайшее)

fld qword [value]      ; 42.3
fistp dword [res]      ; res = 42 (ближайшее)

fld qword [value]      ; 42.5
fistp dword [res]      ; res = 42 (к чётному!)

; Режим можно изменить (см. раздел про
; Control Word)

3. Арифметические операции

Сложение (fadd)
; fadd — сложить два верхних элемента

fld qword [a]      ; ST(0) = 10
fld qword [b]      ; ST(0) = 20, ST(1) = 10
fadd               ; ST(0) = 30 (20+10)
                   ; ST(1) всё ещё 10!

; Или с выгрузкой:
fld qword [a]
fld qword [b]
faddp              ; ST(0) = 30, ST(1) убран!

; Или сразу из памяти:
fld qword [a]
fadd qword [b]     ; ST(0) = a + b
Вычитание (fsub)
; fsub — вычесть из нижнего верхнее

fld qword [a]      ; ST(0) = 10
fld qword [b]      ; ST(0) = 3, ST(1) = 10
fsubp              ; ST(0) = ST(1) - ST(0)
                   ; ST(0) = 10 - 3 = 7 ✅

; Порядок важен!
; fsub вычитает ST(0) из ST(1)

; Обратный порядок:
fld qword [a]      ; 10
fld qword [b]      ; 3
fsubrp             ; ST(0) = ST(0) - ST(1)
                   ; ST(0) = 3 - 10 = -7

Порядок операндов — главная ловушка!

╔══════════════════════════════════════════════════════════════════════╗
║           ПРАВИЛО ЗАПОМИНАНИЯ ПОРЯДКА В ВЫЧИТАНИИ/ДЕЛЕНИИ            ║
╚══════════════════════════════════════════════════════════════════════╝

БЕЗ 'r':  нижнее  МИНУС верхнее  (ST(1) - ST(0))
С 'r':    верхнее МИНУС нижнее   (ST(0) - ST(1))

Пример: хотим вычислить 10 - 3 = 7

+----------------------------------+----------------------------------+
|    Вариант 1: fsub (без 'r')     |     Вариант 2: fsubr (с 'r')     |
+----------------------------------+----------------------------------+
| fld qword [ten]   ; ST(0)=10     | fld qword [three] ; ST(0)=3      |
| fld qword [three] ; ST(0)=3      | fld qword [ten]   ; ST(0)=10     |
|                     ST(1)=10     |                     ST(1)=3      |
| fsubp  ; ST(1)-ST(0)=10-3=7      | fsubrp ; ST(0)-ST(1)=10-3=7      |
|          ✅ Правильно!           |           ✅ Правильно!          |
+----------------------------------+----------------------------------+

**Совет:** загружайте числа в том порядке, который нужен для fsub,
   тогда не понадобится fsubr!
Умножение (fmul)
; fmul — умножить два верхних элемента

fld qword [a]      ; ST(0) = 5
fld qword [b]      ; ST(0) = 3, ST(1) = 5
fmulp              ; ST(0) = 15 (5×3)

; Умножение коммутативно, порядок не важен!
; 5×3 = 3×5 ✅

; На самого себя (возведение в квадрат):
fld qword [x]
fmul st0, st0      ; ST(0) = x²
Деление (fdiv)
; fdiv — разделить нижнее на верхнее

fld qword [a]      ; ST(0) = 10
fld qword [b]      ; ST(0) = 2, ST(1) = 10
fdivp              ; ST(0) = ST(1) / ST(0)
                   ; ST(0) = 10 / 2 = 5 ✅

; Порядок важен (как в вычитании)
; fdiv делит ST(1) на ST(0)

; Обратное деление:
fdivrp             ; ST(0) = ST(0) / ST(1)

4. Специальные математические операции

Корни и степени
; Квадратный корень
fld qword [x]
fsqrt              ; ST(0) = √x

; Квадрат (умножить на себя)
fld qword [x]
fmul st0, st0      ; ST(0) = x²

; Куб (x³)
fld qword [x]
fld st0            ; Копия
fmul st0, st0      ; x²
fmulp              ; x² × x = x³

; Четвёртая степень (x⁴)
fld qword [x]
fmul st0, st0      ; x²
fmul st0, st0      ; (x²)² = x⁴
Прочие операции
; Абсолютное значение
fld qword [x]
fabs               ; ST(0) = |x|

; Смена знака
fld qword [x]
fchs               ; ST(0) = -x

; Округление до целого
fld qword [x]      ; x = 3.7
frndint            ; ST(0) = 4.0

; Извлечение целой и дробной части
fld qword [x]      ; x = 3.7
fld st0            ; Копия
frndint            ; ST(0) = 4.0 (целое)
fsub st1, st0      ; ST(1) = 0.7 (дробное)

5. Трансцендентные функции

Это главное преимущество FPU перед SSE/AVX — встроенные сложные функции:

Тригонометрия
; Все углы вычисляются в радианах

; Синус
fld qword [angle]   ; angle в радианах
fsin                ; ST(0) = sin(angle)

; Косинус
fld qword [angle]
fcos                ; ST(0) = cos(angle)

; Синус И косинус одновременно
fld qword [angle]
fsincos             ; ST(0) = cos, ST(1) = sin
                    ; Быстрее, чем два вызова!

; Тангенс (частичный)
fld qword [angle]
fptan               ; ST(0) = tan(angle)
                    ; ST(1) = 1.0 (доп. значение)
Логарифмы и экспоненты
; Логарифм по основанию 2
fld qword [x]
fld1
fxch
fyl2x               ; ST(0) = log₂(x)

; Натуральный логарифм ln(x)
fld qword [x]
fldln2              ; log₂(e)
fxch
fyl2x               ; log₂(x)
; Теперь нужно разделить...
; (сложно, см. примеры ниже)

; Экспонента 2^x - 1
fld qword [x]
f2xm1               ; ST(0) = 2^x - 1
fld1
faddp               ; ST(0) = 2^x

; Внимание: f2xm1 точна только для |x| <= 1
🤔 Почему fptan возвращает два значения?

Из-за исторических причин fptan помещает на стек ДВА числа:

  1. ST(0) = tan(x) — собственно тангенс
  2. ST(1) = 1.0 — дополнительное значение для совместимости

Обычно единицу просто удаляют:

fld qword [angle]
fptan               ; ST(0) = tan, ST(1) = 1.0
fstp st1            ; Убрать единицу
                    ; ST(0) = tan (нужный результат)

Как перевести градусы в радианы?

Формула: $\text{radians} = \text{degrees} \times \frac{\pi}{180}$

section .data
    degrees dq 45.0        ; 45 градусов
    pi_over_180 dq 0.017453292519943295  ; π/180
    radians dq 0.0

section .text
    fld qword [degrees]
    fld qword [pi_over_180]
    fmulp
    fstp qword [radians]   ; radians ≈ 0.785 (π/4)
    
    ; Теперь можно использовать в fsin/fcos

Или с использованием константы fldpi:

fld qword [degrees]    ; 45
fldpi                  ; π
fmulp                  ; 45π
fld qword [c_180]      ; 180
fdivp                  ; 45π/180 = π/4
fsin                   ; sin(π/4) ≈ 0.707

📝 6. Практические задачи (от простых к сложным)

Задача 1: Вычислить среднее арифметическое

Формула: $\text{average} = \frac{a + b + c}{3}$

Решение с комментариями
section .data
    a dq 10.0
    b dq 20.0
    c dq 30.0
    three dq 3.0
    average dq 0.0

section .text
global _start

_start:
    ; Сложить три числа
    fld qword [a]          ; ST(0) = 10
    fld qword [b]          ; ST(0) = 20, ST(1) = 10
    faddp                  ; ST(0) = 30
    
    fld qword [c]          ; ST(0) = 30, ST(1) = 30
    faddp                  ; ST(0) = 60
    
    ; Разделить на 3
    fld qword [three]      ; ST(0) = 3, ST(1) = 60
    fdivp                  ; ST(0) = 60/3 = 20
    
    fstp qword [average]
    
    ; Выход
    mov rax, 60
    xor rdi, rdi
    syscall
Компактная версия
section .data
    a dq 10.0
    b dq 20.0
    c dq 30.0
    three dq 3.0
    average dq 0.0

section .text
global _start

_start:
    fld qword [a]
    fadd qword [b]         ; Сразу из памяти
    fadd qword [c]
    fld qword [three]
    fdivp
    fstp qword [average]
    
    mov rax, 60
    xor rdi, rdi
    syscall
Состояние стека на каждом шаге
После fld [a]:     │ 10  │
                   └─────┘

После fadd [b]:    │ 30  │  (10 + 20)
                   └─────┘

После fadd [c]:    │ 60  │  (30 + 30)
                   └─────┘

После fld [three]: │ 3   │
                   │ 60  │
                   └─────┘

После fdivp:       │ 20  │  (60 / 3)
                   └─────┘

Задача 2: Формула расстояния между точками

Формула: $d = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$

section .data
    x1 dq 1.0
    y1 dq 2.0
    x2 dq 4.0
    y2 dq 6.0
    distance dq 0.0

section .text
global _start

_start:
    ; Вычислить (x2 - x1)²
    fld qword [x2]         ; ST(0) = 4
    fld qword [x1]         ; ST(0) = 1, ST(1) = 4
    fsubp                  ; ST(0) = 3 (4-1)
    fmul st0, st0          ; ST(0) = 9 (3²)
    
    ; Вычислить (y2 - y1)²
    fld qword [y2]         ; ST(0) = 6, ST(1) = 9
    fld qword [y1]         ; ST(0) = 2, ST(1) = 6, ST(2) = 9
    fsubp                  ; ST(0) = 4, ST(1) = 9
    fmul st0, st0          ; ST(0) = 16 (4²)
    
    ; Сложить и извлечь корень
    faddp                  ; ST(0) = 25 (9+16)
    fsqrt                  ; ST(0) = 5
    
    fstp qword [distance]
    
    mov rax, 60
    xor rdi, rdi
    syscall

Результат: расстояние между (1,2) и (4,6) равно 5.0


Задача 3: Конвертация температуры (Цельсий → Фаренгейт)

Формула: $F = C \times \frac{9}{5} + 32$

Версия 1: Через дроби
section .data
    celsius dq 25.0
    nine dq 9.0
    five dq 5.0
    thirtytwo dq 32.0
    fahrenheit dq 0.0

section .text
global _start

_start:
    ; C × 9/5
    fld qword [celsius]    ; 25
    fld qword [nine]       ; 9
    fmulp                  ; 225
    fld qword [five]       ; 5
    fdivp                  ; 45
    
    ; + 32
    fld qword [thirtytwo]  ; 32
    faddp                  ; 77
    
    fstp qword [fahrenheit]
    
    mov rax, 60
    xor rdi, rdi
    syscall
Версия 2: Через коэффициент 1.8
section .data
    celsius dq 25.0
    coeff dq 1.8           ; 9/5 = 1.8
    thirtytwo dq 32.0
    fahrenheit dq 0.0

section .text
global _start

_start:
    fld qword [celsius]    ; 25
    fld qword [coeff]      ; 1.8
    fmulp                  ; 45
    fld qword [thirtytwo]  ; 32
    faddp                  ; 77
    
    fstp qword [fahrenheit]
    
    mov rax, 60
    xor rdi, rdi
    syscall

Результат: 25°C = 77°F ✅


Задача 4: Площадь треугольника по формуле Герона

Формула: $$p = \frac{a + b + c}{2}$$ $$S = \sqrt{p(p-a)(p-b)(p-c)}$$

section .data
    a dq 3.0               ; Сторона a
    b dq 4.0               ; Сторона b
    c dq 5.0               ; Сторона c (прямоугольный треугольник!)
    two dq 2.0
    area dq 0.0

section .text
    global _start

_start:
    ; Вычислить полупериметр p = (a+b+c)/2
    fld qword [a]
    fadd qword [b]
    fadd qword [c]         ; ST(0) = 12 (a+b+c)
    fld qword [two]
    fdivp                  ; ST(0) = 6 (полупериметр p)
    
    ; Теперь нужно вычислить p(p-a)(p-b)(p-c)
    ; Сохраним s для дальнейшего использования
    fld st0                ; ST(0) = s, ST(1) = p (дубликат)
    
    ; s - a
    fld qword [a]          ; ST(0) = 3, ST(1) = 6, ST(2) = 6
    fsubp                  ; ST(0) = 3 (6-3), ST(1) = 6
    
    ; Умножить на s
    fmul st0, st1          ; ST(0) = 18 (6×3), ST(1) = 6
    
    ; s - b
    fld st1                ; ST(0) = 6, ST(1) = 18, ST(2) = 6
    fld qword [b]          ; ST(0) = 4, ST(1) = 6, ST(2) = 18
    fsubp                  ; ST(0) = 2, ST(1) = 18, ST(2) = 6
    fmulp                  ; ST(0) = 36 (18×2), ST(1) = 6
    
    ; s - c
    fld st1                ; ST(0) = 6, ST(1) = 36
    fld qword [c]          ; ST(0) = 5, ST(1) = 6, ST(2) = 36
    fsubp                  ; ST(0) = 1, ST(1) = 36
    fmulp                  ; ST(0) = 36 (36×1)
    
    ; Извлечь корень
    fsqrt                  ; ST(0) = 6.0
    
    fstp qword [area]
    
    mov rax, 60
    xor rdi, rdi
    syscall

Результат: площадь треугольника 3-4-5 равна 6.0 (для проверки: $\frac{3 \times 4}{2} = 6$ ✅)


Задача 5: Вычислить синус угла 30°

Важно: FPU работает только с радианами! $30° = \frac{\pi}{6}$ радиан.

Версия 1: Вычислить радианы
section .data
    degrees dq 30.0
    pi_over_180 dq 0.017453292519943295
    result dq 0.0

section .text
    global _start

_start:
    ; Градусы → Радианы
    fld qword [degrees]         ; 30
    fld qword [pi_over_180]     ; π/180
    fmulp                       ; 30π/180 = π/6
    
    ; Вычислить синус
    fsin                        ; sin(π/6)
    
    fstp qword [result]         ; ≈ 0.5
    
    mov rax, 60
    xor rdi, rdi
    syscall
Версия 2: Использовать fldpi
section .data
    degrees dq 30.0
    c_180 dq 180.0
    result dq 0.0

section .text
    global _start

_start:
    ; Вычислить радианы: deg×π/180
    fld qword [degrees]    ; 30
    fldpi                  ; π
    fmulp                  ; 30π
    fld qword [c_180]      ; 180
    fdivp                  ; 30π/180 = π/6
    
    fsin                   ; sin(π/6) ≈ 0.5
    
    fstp qword [result]
    
    mov rax, 60
    xor rdi, rdi
    syscall

Результат: sin(30°) = 0.5 ✅

📐 Таблица известных значений для проверки
Угол Радианы sin cos tan
0° 0 0 1 0
30° π/6 0.5 √3/2 ≈ 0.866 √3/3 ≈ 0.577
45° π/4 √2/2 ≈ 0.707 √2/2 ≈ 0.707 1
60° π/3 √3/2 ≈ 0.866 0.5 √3 ≈ 1.732
90° π/2 1 0 ∞

Задача 6: Работа с массивами — сумма элементов

section .data
    array dq 1.5, 2.7, 3.2, 4.8, 5.1    ; 5 элементов
    count equ 5
    sum dq 0.0

section .text
    global _start

_start:
    fldz                        ; ST(0) = 0.0 (аккумулятор)
    mov rcx, count              ; Счётчик цикла
    lea rsi, [rel array]        ; Указатель на массив
    
loop_start:
    fadd qword [rsi]            ; ST(0) += текущий элемент
    add rsi, 8                  ; Следующий элемент (+8 байт)
    loop loop_start             ; Декремент RCX, повтор если != 0
    
    fstp qword [sum]            ; Сохранить сумму
    
    mov rax, 60
    xor rdi, rdi
    syscall

Результат: 1.5 + 2.7 + 3.2 + 4.8 + 5.1 = 17.3

💡 Оптимизация: раскрутка цикла

Для небольших массивов быстрее развернуть цикл:

_start:
    fld qword [array+0]         ; Первый элемент
    fadd qword [array+8]        ; +второй
    fadd qword [array+16]       ; +третий
    fadd qword [array+24]       ; +четвёртый
    fadd qword [array+32]       ; +пятый
    fstp qword [sum]

Это быстрее, потому что:

  • Нет накладных расходов на цикл
  • Процессор может выполнять инструкции параллельно
  • Нет зависимости от счётчика RCX

Задача 7: Квадратное уравнение (дискриминант и корни)

Формула: $ax^2 + bx + c = 0$

$$D = b^2 - 4ac$$ $$x_{1,2} = \frac{-b \pm \sqrt{D}}{2a}$$

section .data
    a dq 1.0               ; Коэффициент a
    b dq -5.0              ; Коэффициент b  
    c dq 6.0               ; Коэффициент c
    two dq 2.0
    four dq 4.0
    discriminant dq 0.0
    x1 dq 0.0
    x2 dq 0.0

section .text
    global _start

_start:
    ; Вычислить дискриминант D = b² - 4ac
    fld qword [b]          ; b
    fmul st0, st0          ; b²
    
    fld qword [four]       ; 4
    fld qword [a]          ; a
    fmulp                  ; 4a
    fld qword [c]          ; c
    fmulp                  ; 4ac
    
    fsubp                  ; b² - 4ac = D
    fld st0                ; Копия D для x2
    fstp qword [discriminant]
    
    ; Вычислить √D
    fsqrt                  ; √D
    
    ; Вычислить x1 = (-b + √D) / 2a
    fld qword [b]          ; b
    fchs                   ; -b
    fld st1                ; √D
    faddp                  ; -b + √D
    fld qword [two]        ; 2
    fld qword [a]          ; a
    fmulp                  ; 2a
    fdivp                  ; (-b + √D) / 2a
    fstp qword [x1]
    
    ; Вычислить x2 = (-b - √D) / 2a
    fld qword [b]          ; b
    fchs                   ; -b
    fxch                   ; Поменять с √D
    fsubp                  ; -b - √D
    fld qword [two]        ; 2
    fld qword [a]          ; a
    fmulp                  ; 2a
    fdivp                  ; (-b - √D) / 2a
    fstp qword [x2]
    
    mov rax, 60
    xor rdi, rdi
    syscall

Для уравнения $x^2 - 5x + 6 = 0$:

  • D = 25 - 24 = 1
  • x₁ = (5 + 1) / 2 = 3
  • x₂ = (5 - 1) / 2 = 2

Проверка: (x-2)(x-3) = x² - 5x + 6 ✅


Задача 8: Вычисление факториала (через цикл)

Задача: вычислить $5! = 1 \times 2 \times 3 \times 4 \times 5 = 120$

section .data
    n dd 5                 ; Вычислить 5!
    result dq 0.0

section .bss
    temp resd 1            ; Временная переменная

section .text
    global _start

_start:
    mov ecx, [n]           ; Счётчик = 5
    fld1                   ; ST(0) = 1.0 (аккумулятор)
    
factorial_loop:
    ; Умножить на текущий счётчик
    push rcx               ; Сохранить RCX
    mov dword [temp], ecx  ; Конвертировать в память
    fild dword [temp]      ; Загрузить как float
    fmulp                  ; Умножить
    pop rcx                ; Восстановить RCX
    
    loop factorial_loop    ; Декремент, повтор если != 0
    
    fstp qword [result]    ; Сохранить результат
    
    mov rax, 60
    xor rdi, rdi
    syscall

Результат: 5! = 120.0


Задача 9: Приближённое вычисление π (формула Лейбница)

Формула: $\frac{\pi}{4} = 1 - \frac{1}{3} + \frac{1}{5} - \frac{1}{7} + \frac{1}{9} - …$

section .data
    iterations dd 1000     ; Количество итераций
    four dq 4.0
    pi_approx dq 0.0

section .bss
    temp_denom resd 1

section .text
    global _start

_start:
    fldz                   ; Сумма = 0
    mov ecx, [iterations]
    mov eax, 1             ; Знаменатель (1, 3, 5, 7...)
    mov ebx, 1             ; Знак (1, -1, 1, -1...)
    
leibniz_loop:
    ; Вычислить текущий член: sign / denominator
    push rax
    mov dword [temp_denom], eax
    fild dword [temp_denom]    ; Знаменатель
    fld1                       ; 1
    fdivrp                     ; 1 / знаменатель
    pop rax
    
    ; Применить знак
    test ebx, ebx
    jns add_term
    fchs                       ; Сменить знак
add_term:
    faddp                      ; Добавить к сумме
    
    ; Следующая итерация
    add eax, 2                 ; 1→3→5→7...
    neg ebx                    ; Сменить знак
    loop leibniz_loop
    
    ; Умножить на 4
    fld qword [four]
    fmulp
    fstp qword [pi_approx]
    
    mov rax, 60
    xor rdi, rdi
    syscall

Результат: После 1000 итераций π ≈ 3.140… (точность улучшается с увеличением итераций)

📊 Сравнение точности
Итераций Результат Ошибка
10 3.04183… ~0.1
100 3.13159… ~0.01
1000 3.14059… ~0.001
10000 3.14149… ~0.0001

Реальный π = 3.14159265358979…

Примечание: Формула Лейбница сходится медленно! Для быстрого вычисления π используйте другие методы (например, Мачина или Чудновского).

🛠️ 7. Распространенные ошибки и решения

Ошибки в FPU-коде часто связаны с неверным управлением стеком. Здесь мы рассмотрим специфичные для FPU проблемы, а более общие ловушки описаны в статье «Топ ошибок в NASM: Почему падает Segfault и неверные расчёты». Для отладки таких проблем полезно использовать GDB, как описано в руководстве по отладке в VS Code.

Ошибка 1: Переполнение стека

❌ Неправильно
; Забыли выгрузить результаты!
fld qword [a]
fld qword [b]
faddp              ; Результат в ST(0)
; ❌ Не сохранили!

fld qword [c]      ; Ещё одно число
fld qword [d]
fmulp
; ❌ Снова не сохранили!

; После 8 таких операций:
; ❌ ПЕРЕПОЛНЕНИЕ СТЕКА!

Проблема:

Стек FPU имеет только 8 регистров (ST0-ST7). Если не выгружать результаты, стек переполнится.

✅ Правильно
; Всегда выгружайте результаты!
fld qword [a]
fld qword [b]
faddp
fstp qword [result1]  ; ✅ Сохранили!

fld qword [c]
fld qword [d]
fmulp
fstp qword [result2]  ; ✅ Сохранили!

; Стек чист, можно продолжать

Правило:

После каждого вычисления сохраняйте результат через fstp, если он больше не нужен в стеке.


Ошибка 2: Путаница с порядком операндов

❌ Ошибка в вычитании
; Хотим: 10 - 3 = 7
fld qword [ten]     ; ST(0) = 10
fld qword [three]   ; ST(0) = 3, ST(1) = 10
fsubp               ; ST(0) = ???

; Результат: ST(0) = 7 ✅
; НО! Порядок загрузки сбивает с толку

Путаница:

fsubp делает ST(1) - ST(0), что правильно, но не интуитивно.

✅ Два способа исправить
; Способ 1: Загружать в нужном порядке
fld qword [ten]     ; ST(0) = 10
fld qword [three]   ; ST(0) = 3, ST(1) = 10
fsubp               ; ST(0) = 10-3 = 7 ✅

; Способ 2: Использовать fsub напрямую
fld qword [ten]     ; ST(0) = 10
fsub qword [three]  ; ST(0) = 10-3 = 7 ✅
; (вычитает из ST(0))

; Способ 3: Обратная операция
fld qword [three]   ; ST(0) = 3
fld qword [ten]     ; ST(0) = 10, ST(1) = 3
fsubrp              ; ST(0) = 10-3 = 7 ✅
╔═══════════════════════════════════════════════════════════════╗
║                  ШПАРГАЛКА: ПОРЯДОК ОПЕРАНДОВ                 ║
╚═══════════════════════════════════════════════════════════════╝

Операция          Что делает         Результат
─────────────────────────────────────────────────────────────────
faddp             ST(1) + ST(0)      Порядок не важен
fmulp             ST(1) × ST(0)      Порядок не важен
fsubp             ST(1) - ST(0)      Нижнее МИНУС верхнее
fsubrp            ST(0) - ST(1)      Верхнее МИНУС нижнее
fdivp             ST(1) / ST(0)      Нижнее ДЕЛИТЬ НА верхнее
fdivrp            ST(0) / ST(1)      Верхнее ДЕЛИТЬ НА нижнее

**Мнемоника:** 'r' = reverse (обратный порядок)

Ошибка 3: Смешивание типов данных

❌ Неправильное указание типа
section .data
    x dd 42        ; Это целое int!

section .text
    fld dword [x]  ; ❌ ОШИБКА!
    ; Попытка загрузить int как float
    ; Получится мусор!

Проблема:

fld ожидает вещественное число, но получает целое. Биты интерпретируются неправильно.

✅ Правильное использование
section .data
    x_int dd 42        ; Целое
    x_float dd 42.0    ; Вещественное

section .text
    ; Для целых используйте fild
    fild dword [x_int]     ; ✅ Загрузка int
    ; ST(0) = 42.0 (конвертировано)
    
    ; Для вещественных используйте fld
    fld dword [x_float]    ; ✅ Загрузка float
    ; ST(0) = 42.0

Правило:

  • fld / fst / fstp — для вещественных чисел
  • fild / fist / fistp — для целых чисел (с автоматической конвертацией)

Ошибка 4: Забыли инициализировать FPU

❌ Без инициализации
global _start

_start:
    ; ❌ Не инициализировали FPU!
    fld qword [x]
    ; Стек может содержать мусор
    fadd qword [y]
    ; Непредсказуемый результат!

Проблема:

После запуска программы состояние FPU неопределено. В стеке может быть мусор.

✅ С инициализацией
global _start

_start:
    finit              ; ✅ Инициализация FPU
    ; Теперь:
    ; - Стек пуст
    ; - Режимы по умолчанию
    ; - Флаги исключений очищены
    
    fld qword [x]
    fadd qword [y]
    ; Гарантированно правильный результат

Правило:

Всегда начинайте с finit для очистки состояния FPU.


Ошибка 5: Неправильная работа с углами

❌ Градусы вместо радиан
section .data
    angle dq 90.0      ; 90 градусов

section .text
    fld qword [angle]
    fsin               ; ❌ ОШИБКА!
    ; sin(90 радиан) ≈ 0.894
    ; Ожидали: sin(90°) = 1.0

Проблема:

FPU принимает углы только в радианах. 90 градусов ≠ 90 радиан!

✅ Конвертация в радианы
section .data
    angle_deg dq 90.0
    pi_over_180 dq 0.017453292519943295

section .text
    ; Градусы → Радианы
    fld qword [angle_deg]      ; 90
    fld qword [pi_over_180]    ; π/180
    fmulp                      ; 90π/180 = π/2
    
    fsin                       ; ✅ sin(π/2) = 1.0

Формула конвертации:

$$\text{radians} = \text{degrees} \times \frac{\pi}{180}$$

🐞 8. Отладка FPU-кода

Полезные техники

Проверка статуса FPU
; Сохранить Status Word в AX
fstsw ax

; Проверить флаги исключений (биты 0-5)
test ah, 0x01      ; Invalid Operation
test ah, 0x02      ; Denormalized Operand
test ah, 0x04      ; Division by Zero
test ah, 0x08      ; Overflow
test ah, 0x10      ; Underflow
test ah, 0x20      ; Precision

; Пример: проверка деления на ноль
fld qword [a]
fld qword [b]
fdivp
fstsw ax
test ah, 0x04
jnz division_by_zero_handler
Использование GDB для отладки
# Скомпилировать с отладочной информацией
nasm -f elf64 -g -F dwarf program.asm
ld -o program program.o

# Запустить в GDB
gdb ./program

# Полезные команды в GDB:
(gdb) info float              # Показать все регистры FPU
(gdb) print $st0              # Значение ST(0)
(gdb) print $st1              # Значение ST(1)
(gdb) info registers fctrl    # Control Word
(gdb) info registers fstat    # Status Word
(gdb) x/8xb &value            # Посмотреть байты float в памяти

# Установить breakpoint перед FPU операцией
(gdb) break *_start+10
(gdb) run
(gdb) info float              # Проверить состояние
Макрос для печати стека (отладочный)
; ВНИМАНИЕ: Это изменяет стек! Только для отладки!
%macro DEBUG_PRINT_STACK 0
    ; Сохранить все 8 регистров в память
    sub rsp, 80
    mov rbx, rsp
    
    ; Выгрузить все элементы
    fstp qword [rbx+0]
    fstp qword [rbx+8]
    fstp qword [rbx+16]
    fstp qword [rbx+24]
    fstp qword [rbx+32]
    fstp qword [rbx+40]
    fstp qword [rbx+48]
    fstp qword [rbx+56]
    
    ; Здесь можно вывести значения через syscall
    ; (для простоты опущено)
    
    ; Восстановить стек в обратном порядке
    fld qword [rbx+56]
    fld qword [rbx+48]
    fld qword [rbx+40]
    fld qword [rbx+32]
    fld qword [rbx+24]
    fld qword [rbx+16]
    fld qword [rbx+8]
    fld qword [rbx+0]
    
    add rsp, 80
%endmacro

Внимание: Этот макрос разрушает и восстанавливает стек. Используйте только в режиме отладки!


Интерпретация Status Word

Регистр Status Word (16 бит) содержит информацию о состоянии FPU:

Биты Status Word (после fstsw ax):

15  14  13-11  10-8   7   6   5   4   3   2   1   0
B   C3  TOP    C2-C0  ES  SF  PE  UE  OE  ZE  DE  IE
│   │   │      │      │   │   │   │   │   │   │   │
│   │   │      │      │   │   │   │   │   │   │   └─ Invalid Operation
│   │   │      │      │   │   │   │   │   │   └───── Denormal
│   │   │      │      │   │   │   │   │   └───────── Zero Divide
│   │   │      │      │   │   │   │   └───────────── Overflow
│   │   │      │      │   │   │   └───────────────── Underflow
│   │   │      │      │   │   └───────────────────── Precision
│   │   │      │      │   └───────────────────────── Stack Fault
│   │   │      │      └───────────────────────────── Exception Summary
│   │   │      └──────────────────────────────────── Condition codes
│   │   └─────────────────────────────────────────── Top of stack pointer
│   └─────────────────────────────────────────────── Condition code C3
└─────────────────────────────────────────────────── Busy flag

Пример проверки:

fstsw ax          ; Загрузить Status Word
sahf              ; Скопировать AH в флаги процессора
jz result_zero    ; Переход если результат = 0
jc result_less    ; Переход если результат < 0

⚡ 9. Оптимизация производительности

Таблица латентностей инструкций

Инструкция Латентность (циклы) Throughput Примечание
fld 1-3 1/цикл Зависит от кэша
fst / fstp 1-3 1/цикл
fadd / fsub 3-5 1/цикл
fmul 5-7 2/цикл
fdiv 20-45 20/инстр Очень медленно!
fsqrt 15-30 15/инстр
fsin / fcos 50-120 50/инстр Избегайте в циклах
fpatan 30-80 30/инстр
fyl2x 30-60 30/инстр

Латентность = сколько тактов проходит от начала до завершения

Throughput = как часто можно запускать новую инструкцию


Советы по оптимизации

❌ Медленный код
; Избыточные загрузки/выгрузки
loop_start:
    fld qword [array + rsi]
    fstp qword [temp]
    fld qword [temp]
    fadd qword [sum]
    fstp qword [sum]
    add rsi, 8
    loop loop_start
; Много обращений к памяти!

Проблемы:

  • Ненужные fstp / fld пары
  • Каждая итерация обращается к памяти 4 раза
  • Стек постоянно опустошается
✅ Оптимизированный код
; Держим аккумулятор в стеке
fldz                ; sum = 0 (остаётся в ST0)
loop_start:
    fadd qword [array + rsi]  ; Прямо в ST(0)
    add rsi, 8
    loop loop_start
fstp qword [sum]    ; Один раз в конце
; Минимум обращений к памяти!

Улучшения:

  • Аккумулятор живёт в ST(0)
  • Одно обращение к памяти на итерацию
  • Нет лишних операций со стеком

Избегайте деления — используйте умножение

❌ Медленное деление
; Деление в цикле (медленно!)
loop_start:
    fld qword [array + rsi]
    fld qword [divisor]    ; 5.0
    fdivp                  ; 20-45 тактов!
    fstp qword [result + rsi]
    add rsi, 8
    loop loop_start

Проблема:

fdiv выполняется 20-45 тактов. В цикле на 1000 элементов = 20000-45000 тактов!

✅ Умножение на обратное
; Предвычислить обратное значение
fld1
fld qword [divisor]        ; 5.0
fdivp                      ; 1/5 = 0.2 (один раз!)
fstp qword [inv_divisor]

; Теперь умножаем в цикле
loop_start:
    fld qword [array + rsi]
    fmul qword [inv_divisor]  ; 5-7 тактов
    fstp qword [result + rsi]
    add rsi, 8
    loop loop_start

Выигрыш:

5-7 тактов вместо 20-45. В 4-6 раз быстрее!


Раскрутка циклов

Обычный цикл
; 5 итераций с накладными расходами
mov rcx, 5
loop_start:
    fld qword [array + rsi]
    fadd st0, st1
    fstp qword [array + rsi]
    add rsi, 8
    loop loop_start

Накладные расходы на каждой итерации:

  • Декремент RCX
  • Проверка условия
  • Переход (может быть предсказан неверно)
Раскрученный цикл
; Развернули цикл
fld qword [array+0]
fadd st0, st1
fstp qword [array+0]

fld qword [array+8]
fadd st0, st1
fstp qword [array+8]

fld qword [array+16]
fadd st0, st1
fstp qword [array+16]

fld qword [array+24]
fadd st0, st1
fstp qword [array+24]

fld qword [array+32]
fadd st0, st1
fstp qword [array+32]

Преимущества:

  • Нет накладных расходов цикла
  • Процессор может выполнять параллельно
  • Предсказание переходов не нужно

Примечание: Раскрутка увеличивает размер кода. Используйте для малых фиксированных размеров.

🔭 10. Взгляд в будущее: SSE и AVX

Хотя FPU (x87) отлично подходит для обучения и сложных тригонометрических вычислений, в современном программировании под x86-64 стандартом де-факто являются SSE (Streaming SIMD Extensions) и AVX.

Почему FPU считается устаревшим?

  1. Стековая модель: Неудобно управлять регистрами (нужно постоянно делать fxch, fstp).
  2. Скорость: FPU работает с одним числом за раз.
  3. Совместимость: 64-битные компиляторы C/C++ (GCC, Clang) по умолчанию используют SSE регистры (xmm0-xmm15) даже для одиночных вычислений float и double.

Сравнение подходов

Классический FPU (x87)
; Сложение двух чисел
fld qword [a]    ; Загрузить a в стек
fadd qword [b]   ; Добавить b
fstp qword [res] ; Сохранить и очистить стек
Современный SSE (SIMD)
; Сложение двух чисел
movsd xmm0, [a]   ; Загрузить a в регистр xmm0
addsd xmm0, [b]   ; Добавить b в xmm0
movsd [res], xmm0 ; Сохранить результат

Когда переходить на SSE? Как только вы освоите работу с памятью и базовую логику ассемблера. SSE позволяет складывать по 4 (float) или 2 (double) числа одной инструкцией, что критически важно для обработки аудио, видео и игровой физики.

📦 Готовое решение: Чтобы не писать сложный код ввода/вывода float вручную, используйте готовый модуль io_float.asm, описанный в статье «Ввод-вывод на чистом NASM: Готовые модули (без libc)».

ИТОГ

  • FPU (x87) используйте для: трансцендентной математики (sin, cos, log), максимальной точности (80 бит), одиночных сложных вычислений.
  • SSE/AVX лучше для: массовой обработки данных, простой арифметики, современных приложений с параллелизмом.
💜

Полезный материал?

Если статья помогла разобраться в теме, вы можете поддержать автора любой комфортной суммой.

Поддержать через CloudTips