Сравнение чисел с плавающей точкой

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

Большинство инструкций, которые работают с числами с плавающей точкой, никак не обновляют условные флаги. Но есть специальная инструкция FCMP, которая сравнивает числа с плавающей точкой и обновляет соответсвующим образом условные флаги. Эта инструкция имеет следующие формы:

FCMP Hd, Hm
FCMP Hd, #0.0
FCMP Sd, Sm
FCMP Sd, #0.0
FCMP Dd, Dm
FCMP Dd, #0.0

Эта инструкция может сравнивать два 16-разрядных регистра H, два 32-разрядных регистра S, два 64-разрядных регистра D. Она также может сравнивать значение регистра с конкретным числом. В итоге инструкция FCMP вычитает операнды и обновляет флаги.

Например, сравним два числа

// Пример программы, которая сравнивает два числа с плавающей точкой
.global _start

_start: 
    LDR X4, =number1
    LDP S0, S1, [X4], #8    // загружаем числа number1 и number2 в S0 и S0
    FCMP S0, S1             // сравниваем числа в S0 и S1
    B.GE else              // если S0 больше чем S1, переход к метке else
    
    LDR X1, =less          // строка для вывода, если S0 меньше чем S1
    MOV X2, #16             // длина строки
    B endif                // переход к метке endif для завершения условной конструкции
 
else:                      // если S0 меньше чем S1
    LDR X1, =greater       // строка для вывода, если S0 больше чем S1
    MOV X2, #19             // длина строки

endif:                     // вывод сообщения на консоль
    MOV X0, #1              // 1 = StdOut - поток вывода
    MOV X8, #64             // устанавливаем функцию Linux для вывода строки
    SVC 0                   // вызываем функцию

    MOV X0, #0                 // код возврата из функции - 0
    MOV X8, #93                // номер функции Linux для выхода из программы - 93
    SVC 0                      // вызываем функцию и выходим из программы
.data
    number1: .single 2.08
    number2: .single 0.16
    greater: .asciz "S0 greater than S1\n"
    less: .asciz "S0 less than S1\n"

Здесь загружаем в регистр S0 число number1, а в регистр S1 - число number2 и сравниваем их. В зависимости от сравнения переходим к определенной метке. Поскольку в данном случае number1 больше чем number2, то на консоль будет выведена строка

S0 greater than S1

Но стоит отметить, что сравнение двух чисел с плавающей точкой на равенство, поскольку нередко два числа близки по значению, но не полностью равны. Например, 1 доллар и 1 цент равны 1 доллару или нет? В разных ситуациях ответ может отличаться. Стандартным решением в этом случае является установка некоторого допустимого отклонения. И если разница между числа меньше этого этого отклонения, то числа считаются равными. Например, определим допустимое отклонение e = 0.000001. Тогда числа в двух регистрах считаются равными, если

abs(S1 - S2) < e

Здесь abs() - это функция для вычисления абсолютного значения.

Сначала посмотрим на возможную проблему. Например, первое число равно 1.0, а второе равно 0.0. Математически если ко второму числу сто раз прибавить 0.01, то мы можем получить первое число, то есть 0.01 * 100 = 1.0. Но это математически, посмотрим как все это будет выглядеть в программе на ассемблере:

.global main
main:
    STR LR, [SP, #-16]!
    LDR X0, =increment          // загружаем указатель на число increment
    LDR S0, [X0], #4            // загружаем в S0 число increment (0.01)
    LDR S1, [X0], #4            // загружаем в S1 число number1 (0.0)
    LDR S2, [X0]                // загружаем в S2 число number2 (1.0)

    MOV W1, #100
loop:
    FADD S1, S1, S0     // сто раз прибавляем S0 (0.01) к S1(0.0)
    SUBS W1, W1, #1     // уменьшаем счетчик цикла
    B.NE loop           // если не достигли 100, переходим обратно к loop

    // в этой точке программы ожидаем, что S1 равно 1.0
    FCMP S1, S2             // сравниваем S1 и S2
    B.EQ equal             // если числа равны, переходим к метке equal
    LDR X0, =notequalstr    // если не равны, загружаем строку notequalstr
    B next
equal: 
    LDR X0, =equalstr   // если равны, загружаем строку equalstr
next:
    BL printf           // вывод на экран с помощью функции printf
    MOV X0, #0      // код возврата из функции
    LDR LR, [SP], #16
    RET

.data
    increment: .single 0.01
    number1: .single 0.0
    number2: .single 1.0
    equalstr: .asciz "S1 and S2 are equal\n"
    notequalstr: .asciz "S1 and S2 are NOT equal\n"

Здесь в регистр S1 загружается число number1 (0.0), и затем к нему в цикле 100 раз прибавляется число из S0 (то есть increment или 0.001).

    MOV W1, #100
loop:
    FADD S1, S1, S0     // сто раз прибавляем S0 (0.01) к S1(0.0)
    SUBS W1, W1, #1     // уменьшаем счетчик цикла
    B.NE loop           // если не достигли 100, переходим обратно к loop
// в этой точке программы ожидаем, что S1 равно 1.0
    FCMP S1, S2             // сравниваем S1 и S2

То есть в конце мы ожидаем, что в S1 будет число 1.0. И затем это число сравниваем с числом из регистра S1 (number1), которое изначально равно 1.0. То есть ожидаем, что числа в S2 и S1 будут равны.

Для упрощения вывода на консоль здесь применяется функция printf, соответственно для компиляции применяется компилятор gcc.

Однако, если мы запустим программу на выполнение, то увидим, что числа не равны:

S1 and S2 are NOT equal

Посмотрим детально, что у нас будет в регистрах после стократного сложения:

.macro printRegister reg        // макрос для вывода содержимого регистра
    STR LR, [SP, #-16]!
    STP D0, D1, [SP, #-16]!
    STP D2, D3, [SP, #-16]!
    FCVT D0, S\reg              // преобразуем single в double
    FMOV X2, D0                 // помещаем число из D0 для вывода на консоль
    MOV X1, #\reg               // помещаем номер регистра для вывода на консоль
    ADD X1, X1, #'0'            // для установки символа для спецификатора %c
    LDR X0, =printStr            // строка форматирования
    BL printf                   // вызываем функцию printf
    LDP D2, D3, [SP], #16
    LDP D0, D1, [SP], #16
    LDR LR, [SP], #16
.endm

.global main
main:
    STR LR, [SP, #-16]!
    LDR X0, =increment          // загружаем указатель на число increment
    LDR S0, [X0], #4            // загружаем в S0 число increment (0.01)
    LDR S1, [X0], #4            // загружаем в S1 число number1 (0.0)
    LDR S2, [X0]                // загружаем в S2 число number2 (1.0)

    MOV W1, #100
loop:
    FADD S1, S1, S0     // сто раз прибавляем S0 (0.01) к S1(0.0)
    SUBS W1, W1, #1     // уменьшаем счетчик цикла
    B.NE loop           // если не достигли 100, переходим обратно к loop

    // в этой точке программы ожидаем, что S1 равно 1.0
    printRegister 1     // вывод регистра S1
    printRegister 2     // вывод регистра S1

    MOV X0, #0      // код возврата из функции
    LDR LR, [SP], #16
    RET

.data
    increment: .single 0.01
    number1: .single 0.0
    number2: .single 1.0
    printStr: .asciz "S%c = %f\n"

Для упрощения вывода содержимого регистра я добавил специальный макрос. При выполнении программы мы увидим содержимое регистров:

S1 = 0.999999
S2 = 1.000000

Мы видим, что число из S2 почти равно числу из S1, но полного равенства нет. Однако разница настолько невелика, что в большинстве случаев ей можно пренебречь. И теперь применим допустимое отклонение

.global main
main:
    STR LR, [SP, #-16]!
    LDR X0, =increment          // загружаем указатель на число increment
    LDR S0, [X0], #4            // загружаем в S0 число increment (0.01)
    LDR S1, [X0], #4            // загружаем в S1 число number1 (0.0)
    LDR S2, [X0], #4            // загружаем в S2 число number2 (1.0)
    LDR S3, [X0]                // загружаем в S3 число epsilon (0.00001) - допустимое отклонение

    MOV W1, #100
loop:
    FADD S1, S1, S0     // сто раз прибавляем S0 (0.01) к S1(0.0)
    SUBS W1, W1, #1     // уменьшаем счетчик цикла
    B.NE loop           // если не достигли 100, переходим обратно к loop

    FSUB S1, S1, S2     // находим разность между S1 и S2
    FABS S1, S1         // получаем абсолютное значение
    FCMP S1, S3         // сравниваем S1 и S3 (допустимое отклонение)
    B.LE equal          // если числа равны, переходим к метке equal
    LDR X0, =notequalstr    // если не равны, загружаем строку notequalstr
    B next
equal: 
    LDR X0, =equalstr   // если равны, загружаем строку equalstr
next:
    BL printf           // вывод на экран с помощью функции printf
    MOV X0, #0      // код возврата
    LDR LR, [SP], #16
    RET

.data
    increment: .single 0.01
    number1: .single 0.0
    number2: .single 1.0
    epsilon: .single 0.00001    // допустимое отклонение
    equalstr: .asciz "S1 and S2 are equal\n"
    notequalstr: .asciz "S1 and S2 are NOT equal\n"

Здесь отклонение задано с помощью метки epsilon, которое загружается в регистр S3:

epsilon: .single 0.00001

То есть если разница между числами S1 и S2 будет равна или меньше этого отклонения, то будем считать, что числа равны

FSUB S1, S1, S2     // находим разность между S1 и S2
FABS S1, S1         // получаем абсолютное значение
FCMP S1, S3         // сравниваем S1 и S3 (допустимое отклонение)

И в этом случае получим другой результат.

Функция сравнения чисел

Поскольку сравнение чисел - довольно распространенная задача, то лучше определить соответствующий функционал в виде отдельнй функции. Например, определим следующий файл fpcompare.s

// Функция для сравнения двух чисел с плавающей точкой
// Входные параметры:
// X0 - указатель на три числа с плавающей точкой number1, number2, epsilon
// Результат:
// X0 - 1, если числа равны, и 0, если не равны
.global fpcompare
fpcompare: 
    LDP S0, S1, [X0], #8   // загружаем 3 числа и увеличиваем адрес в X0 на 8 байт
    LDR S2, [X0]
    // S0 = number1, S1 = number2, S2 = epsilon
    FSUB S3, S1, S0         // S3 = S1 - S0 = number2 - number1
    FABS S3, S3             // получаем абсолютное значение
    FCMP S3, S2             // сравниваем с отклонением
    B.GT notequal           // если абсолютное значение больше допустимого отклонения, переход на метку notequal
    MOV X0, #1              // если равны, помещаем в X0 число 1
    B done                  // переходим к выходу из функции
notequal:
    MOV X0, #0              // если не равны, помещаем в X0 число 0
done: 
    RET

Здесь функция fpcompare получает через регистр X0 указатель на три числа, перва два из которых - сравниваемые числа, а третье число - допустимое отклонение.

В самой функции вычитаем второе число из первого, получаем абсолютное значение разности и сравниваем с отклонением. Если числа равны, то в качестве результата помещаем в X0 число 1, если не равны, то число 0.

В файле main.s, который будет представлять основной файл программы, используем эту функцию:

.global main
main:
    STR LR, [SP, #-16]!
    LDR X0, =increment             // загружаем указатель на число increment
    LDR S0, [X0], #4            // загружаем в S0 число increment (0.01)
    LDR S1, [X0], #4            // загружаем в S1 число number1 (0.0)
    LDR S2, [X0], #4            // загружаем в S2 число number2 (1.0) 
    //LDR S3, [X0]                // загружаем в S3 число epsilon - допустимое отклонение 

    MOV W1, #100
loop:
    FADD S1, S1, S0     // сто раз прибавляем S0 (0.01) к S1(0.0)
    SUBS W1, W1, #1     // уменьшаем счетчик цикла
    B.NE loop           // если не достигли 100, переходим обратно к loop

    LDR X0, =number1     // загружаем указатель на число number1
    STR S1, [X0]        // сохраняем в number1 число из S1
    BL fpcompare           // вызываем функцию сравнения
    CMP X0, #1             // сравниваем результат 
    B.EQ equal             // если равно 1, то есть числа равны, переходим к метке equal
    LDR X0, =notequalstr    // если не равны, загружаем строку notequalstr
    B next
equal: 
    LDR X0, =equalstr   // если равны, загружаем строку equalstr
next:
    BL printf           // вывод на экран с помощью функции printf
    MOV X0, #0      // код возврата из функции
    LDR LR, [SP], #16
    RET

.data
    increment: .single 0.01
    number1: .single 0.0
    number2: .single 1.0
    epsilon: .single 0.00001    // допустимое отклонение
    equalstr: .asciz "S1 and S2 are equal\n"
    notequalstr: .asciz "S1 and S2 are NOT equal\n"
 

Здесь опять же у нас есть два числа number1 и number2. Для тестирования также сто раз прибавляем число increment к number1. Затем сохраняем полученное значение из регистра S1 обратно в number1 и загружаем адрес этого числа в регистр X0 для передачи в функцию fpcompare

LDR X0, =number1     // загружаем указатель на число number1
STR S1, [X0]        // сохраняем в number1 число из S1
BL fpcompare           // вызываем функцию сравнения

Поскольку программу использует функционал языка С, скомпилируем ее с помощью команды

aarch64-none-linux-gnu-gcc main.s fpcompare.s -o main -static
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850