Числа с плавающей точкой или floating-point numbers представляют запись вещественных чисел, в которой число хранится в виде мантиссы и порядка (показателя степени), например,
1.12345 x 102
Для определения чисел с плавающей точкой процессоры ARM используют стандарт IEEE 754, согласно которому каждое число содержит ряд компонентов:
бит знака, который указывает, является ли число положительным или отрицательным
мантисса, в примере выше 1.12345
экспонента или показатель степени, в примере 102
это числа 2.
Процессор ARM работает с числами с плавающей точкой разного размера:
Половинной точности (half-precision), которые занимают 16 бит (поддерживается начиная с архитектуры ARMv8.2)
Одинарной точности (single-precision), которые занимают 32 бита
Двойной точности (double-precision), которые занимают 64 бита
Также числа с плавающей точкой характеризуются таким показателем, как десятичные знаки (decimal digits) или еще называют "десятичная точность" (decimal precision). Этот показатель указывает, сколько десятичных знаков может хранить число. Например, в числе 123.45 десятичная точность равна 5 (5 цифр).
Тип | Размер | Бит знака | Дробная часть | Экспонента | Кол-во десятичных знаков |
Half | 16 бит | 1 | 10 | 5 | 3 |
Single | 32 бита | 1 | 23 | 8 | 7 |
Double | 64 бита | 1 | 52 | 11 | 16 |
Ассемблер GNU предоставляет директивы .single и .double для определения чисел с плавающей точкой одинарной и двойной точности соответственно. Однако отстутствуют директивы для создания 16-разрядных чисел с половинной точностью отсутстве. Пример определения чисел:
.single 1.343, 4.343e20, -0.4343, -0.4444e-10 .double -4.24322322332e-10, 3.141592653589793
Директивы .single
и .double
определяют числа в десятичной системе. Но на уровне памяти числа с плавающей точкой
представляют числа в двоичной системе, десятичное число 0.1 представляет повторяющуюся бинарную последовательность 0.00011001100110011...
,
что усложняет операции с числами с плавающей точкой и нередко приводит к ошибкам в округлениях, когда вовлекается много вычислений.
Для операций с числами с плавающей точкой применяется FPU или Floating-point Unit. Для работы с числами с плавающей точкой он применяет отдельный набор специальных регистров, которые предназначены для другого элемента в архитектуре ARM - сопроцессора NEON, который применяется в параллельных вычислениях.
Итак, сопроцессор NEON имеет 32 128-разрядных регистра, которые называются V0, V1, V2, ... V31
Однако для FPU доступны только младшие 64 бит регистров Vn. Эти 64-разрядные регистры называются D0, D1, D2, ... D31
Также FPU может обращаться к младшим 32 битам регистров как к регистрам S0, S1, S2, ... S31
И также можно обращаться к младшим 16 битам этих регистров как к регистрам H0, H1, H2, ... H31
То есть, если мы, к примеру, возьмем регистр H1, то он представляет младшие 16 бит регистра S1, который, в свою очередь, представляет младшие 32 бита регистра D1, который представляет младшие 64 бита 128-разрядного регистра V1
Опять же отмечу, что хотя на аппаратном уровне имеем 128-разрядные регистры Vn, но для FPU доступны только младшие 64-разрядные регистры Dn (либо 32-разрядные Sn, либо 16-разрядные Hn)
Загрузка и сохранение данных производится, как и в общем случае, с помощью инструкций LDR/LDP и STR/STP соответственно.
LDR X1, =single1 // загружаем в X1 адрес числа single1 LDR S4, [X1] // загружаем в S1 число, которое хранится по адресу в X1 - single1 LDR D5, [X1, #4] // к адресу в X1 прибавляем 4 байта и загружаем число с этого адреса в D5 - double1 STR S4, [X1] // сохраняем по адресу X1 значение из S4 STR D5, [X1, #4] // сохраняем по адресу X1 + 4 значение из D5 .data single1: .single 3.14159 double1: .double 4.5678 single2: .single 0.0 double2: .double 0.0
Инструкция FMOV позволяет перемещать данные между регистрами FPU и CPU:
FMOV H1, W2 // Из W2 в H1 FMOV W2, H1 // Из W2 в H1 FMOV S1, W2 // Из S1 в W2 FMOV X1, D1 // Из X1 в D1 FMOV D3, D4 // Из D3 в D4
При этом обычно сохраняется соответствие по разрядности - число из 64-разрядного регистра перемещаеься в другой 64-разрядный регистр, или число из 32-разрядного регистра помещается в другой 32-разрядный регистр. Однако H-регистры - исключение, их числа можно копировать в регистры большего размера и наоборот.
Для примера определим и выведем на консоль число типа double. Для упрощения вывода воспользуемся функцией printf
языка С.
Пусть файл программы называется main.s и имеет следующий код:
.global main main: STR LR, [SP, #-16]! LDR X0, =number // загружаем указатель на число number LDR D0, [X0] // загружаем данные по адресу из X0 в регистр D0 FMOV X1, D0 // помещаем число double в X1 для форматированного вывода в printf LDR X0, =prtstr // загружаем строку форматирования BL printf // вызываем функцию printf - выводим на консоль полученное расстояние MOV X0, #0 // код возврата LDR LR, [SP], #16 RET .data number: .double 6.5 prtstr: .asciz "number: %f\n"
В данном случае число проецируется на метку number
. Для его получения вначале загружаем адрес метки в регистр X0
LDR X0, =number
Затем в регистр D0 загружаем непосредственно данные, которые хранятся по адресу из X0:
LDR D0, [X0]
Затем для вывода на консоль помещаем число в регистр X1:
FMOV X1, D0
Поскольку файл main.s
использует функционал языка С, то скопилируем приложение с помощью компилятора gcc
:
aarch64-none-linux-gnu-gcc main.s -o main -static
В итоге при выполнении программы на консоль будет выведено:
number: 6.500000