FPU, числа с плавающей точкой и сопроцессор Neon

Определение и загрузка чисел с плавающей точкой

Последнее обновление: 20.01.2023

Числа с плавающей точкой или 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..., что усложняет операции с числами с плавающей точкой и нередко приводит к ошибкам в округлениях, когда вовлекается много вычислений.

ARM FPU

Для операций с числами с плавающей точкой применяется FPU или Floating-point Unit. Для работы с числами с плавающей точкой он применяет отдельный набор специальных регистров, которые предназначены для другого элемента в архитектуре ARM - сопроцессора NEON, который применяется в параллельных вычислениях.

Итак, сопроцессор NEON имеет 32 128-разрядных регистра, которые называются V0, V1, V2, ... V31

регистры сопроцессора NEON и FPU в архитектуре ARM 64

Однако для 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)

Основные команды для работы с FPU

Загрузка и сохранение данных

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

Инструкция 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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850