Числа с плавающей точкой и SIMD
Контекст: x86-64, NASM, Linux. Целочисленная арифметика — основы и длинные числа. Примеры в справочнике (сумма массива SSE).
Два мира вещественной арифметики на x86
| Подсистема | Регистры | Статус сегодня |
|---|---|---|
| x87 FPU | стек ST(0)..ST(7) | legacy; встречается в старом коде и некоторых ABI |
| SSE / SSE2 | XMM0–XMM15 (128 бит) | стандарт для float/double в 64-битных ОС |
| AVX / AVX2 | YMM0–YMM15 (256 бит) | векторные пакеты по 8 float или 4 double |
| AVX-512 | ZMM (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
XMM0–XMM7 в типичном вызове передают вещественные аргументы (System V: XMM0–XMM7). Возврат 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 регистры XMM0–XMM15 — caller-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).
Связанные материалы
- Побайтовые циклы без SIMD — строковые инструкции.
- Разбор бинарника с XMM-операциями — чтение листинга.
- Windows и другой ABI для вызовов — WinAPI и x64.
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Полный отказ от высокоуровневых языков нецелесообразен. Поэтому большинство компиляторов поддерживают встроенный ассемблер — механизм вставки ассемблерных инструкций непосредственно в код на C/C++. %macro, %define и %if в NASM — шаблоны инструкций без дублирования исходного текста. Разделение программы на .asm-файлы, global и extern, сборка объектников и линковка в ELF. Вызов ассемблерных функций из C и наоборот — System V AMD64 ABI, выравнивание стека, сборка. Секции ELF, символы, objdump и сопоставление дизассемблирования с исходным NASM-кодом. REP, MOVS, SCAS, STOS, флаг DF и доступ к данным по индексу через таблицу. Microsoft x64 calling convention, shadow space, вывод в консоль и файлы через API вместо syscall. Основы ассемблера - синтаксис Intel/AT&T, базовые инструкции и принципы низкоуровневого программирования. Архитектура ассемблерных программ - взаимодействие с ОС, вызовы библиотек и организация низкоуровневого кода. Типизация, набор правил определения типа данных значений языка. Управляющие конструкции и команды процессора в ассемблере - регистр команд, переходы и управление потоком исполнения. Команды и подпрограммы в ассемблере - передача параметров, соглашения вызовов и работа со стеком.История ассемблерных языков
Макросы и условная сборка
Несколько модулей и линковка
Взаимодействие с C и C++
Чтение исполняемого файла и листинга
Строковые инструкции и таблицы поиска
Windows x64, WinAPI и отличия от Linux
Основы ассемблера
Архитектура ассемблерных программ
Типы данных и регистры
Управляющие конструкции и команды процессора
Команды и подпрограммы