Nikita Mandrykin

Практикуюсь в программировании. C • Python • Assembly • Linux

Назад

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 Дж

🎮 3D графика и игры

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

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

Нормализация = приведение длины вектора к 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)

Загрузка из памяти
; 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

Загрузка целых чисел (fild)

Обратите внимание на букву 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!

Сохранение в память

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 — сохранить как целое
; 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)

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

Сложение и вычитание

Сложение (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)

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

Корни и степени
; Квадратный корень
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 (дробное)

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

Это главное преимущество 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 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 от основ стековой архитектуры до практического применения. Теперь вы умеете работать с вещественными числами, использовать трансцендентные функции и оптимизировать код.

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

SSE/AVX лучше для: массовой обработки данных, простой арифметики, современных приложений с параллелизмом.

💜

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

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

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