Перейти к основному содержимому

Числа с плавающей точкой и SIMD

Разработчику Архитектору

Контекст: x86-64, NASM, Linux. Целочисленная арифметика — основы и длинные числа. Примеры в справочнике (сумма массива SSE).

Два мира вещественной арифметики на x86

ПодсистемаРегистрыСтатус сегодня
x87 FPUстек ST(0)..ST(7)legacy; встречается в старом коде и некоторых ABI
SSE / SSE2XMM0XMM15 (128 бит)стандарт для float/double в 64-битных ОС
AVX / AVX2YMM0YMM15 (256 бит)векторные пакеты по 8 float или 4 double
AVX-512ZMM (512 бит)серверные и HPC-линейки

Новый ассемблерный код для float почти всегда использует SSE2 (movss, addss, mulsd …), а не стек x87.


Формат IEEE 754

  • single (float) — 32 бита: знак, экспонента, мантисса.
  • double — 64 бита.

В памяти на x86 — little-endian, как целые. Константы в NASM:

section .data
pi dd 3.141592 ; 32-bit float
e dq 2.718281828 ; 64-bit double

Сравнение и порядок — по правилам IEEE; флаги целочисленного CMP на float не подходят — используют COMISS/UCOMISS или сравнение через вычитание с проверкой статуса.


Регистры XMM

XMM0XMM7 в типичном вызове передают вещественные аргументы (System V: XMM0XMM7). Возврат float/double — часто в XMM0.

Один регистр XMM — 128 бит. Может хранить:

  • один double (нижние 64 бита),
  • два float (пакет),
  • четыре int32 (SSE2 integer pack),
  • шестнадцать int8 (для SIMD-обработки байт).

Скалярные операции SSE (один float)

section .data
a dd 1.5
b dd 2.0
r dd 0.0

section .text
movss xmm0, [rel a]
addss xmm0, [rel b] ; xmm0 = 3.5
movss [rel r], xmm0

Суффикс ss — scalar single (один float в младшей части XMM). Для double — sd: movsd, addsd, mulsd.

Обнулить верхние биты XMM после «грязных» операций иногда требуют xorps xmm0, xmm0 / movss — иначе старые данные в старших ланах влияют на некоторые packed-операции.


Векторное сложение (несколько float за раз)

section .data
align 16
vec_a dd 1.0, 2.0, 3.0, 4.0
vec_b dd 10.0, 20.0, 30.0, 40.0

section .text
movaps xmm0, [rel vec_a]
addps xmm0, [rel vec_b] ; четыре сложения параллельно
movaps [rel vec_a], xmm0

movaps требует адрес, кратный 16. Невыровненный доступ — movups (медленнее на старых CPU) или выравнивание буфера через .align 16.


Сравнение и ветвления

movss xmm0, [a]
movss xmm1, [b]
ucomiss xmm0, xmm1 ; сравнить, установить ZF/PF/CF
ja greater ; «выше» для упорядоченных float

Для сложной логики чаще вызывают код на C или используют маски SIMD (cmpps + movmskps), чтобы избежать серии переходов.


Сохранение XMM при вызовах

По System V AMD64 регистры XMM0XMM15caller-saved (вызываемая функция может их портить). Если нужны после call — сохраняйте на стек (movdqu [rsp], xmm0 …) или не трогайте до возврата.

При смешивании с C-кодом с плавающей точкой соблюдайте то же правило, что для целочисленного ABI.


Кратко про стек x87

Инструкции FLD, FADD, FSTP работают со стеком ST. Это другая модель: задержки, сложность оптимизации, отдельные правила округления. В 32-битном Windows раньше встречалось в «чистом» asm; в 64-битном коде компиляторы генерируют SSE.

Для чтения legacy-бинарников достаточно знать: длинные цепочки fld/fistp — почти наверняка x87.


AVX и AVX2

YMM — 256 бит: восемь float или четыре double за операцию (vaddps, vmovaps с префиксом VEX).

Требования:

  • выравнивание 32 байта для vmovaps по памяти;
  • проверка поддержки CPU (cpuid);
  • сохранение YMM при переключении контекста потока — ответственность ОС; в user-коде при вызовах — смотреть ABI.

Для учебного asm чаще достаточно SSE2; AVX имеет смысл в горячих циклах обработки массивов (графика, ML, кодеки).


Десятичная арифметика (BCD)

Редкий случай: упакованный BCD для финансовых расчётов без ошибок двоичного float. x86 имеет инструкции DA/DF (x87) и арифметику десятичных полей. В современном прикладном коде обычно используют целые в минимальных денежных единицах (копейки) и длинную целую, а не BCD.


Когда писать SIMD руками

Имеет смысл, если:

  • профилировщик показал горячий цикл на массивах фиксированного размера;
  • нужна детерминированная последовательность инструкций (ядро, драйвер);
  • компилятор не векторизует из-за aliasing или сложных границ.

В остальных случаях пишут на C/C++/Rust с -O3 -march=native и смотрят листинг (gcc -S).


Связанные материалы


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).