Я пишу протокол, который использует RFC 7049 как его двоичное представление. Стандарт гласит, что протокол может использовать 32-битное представление чисел с плавающей запятой, если их числовое значение эквивалентно соответствующим 64-битным числам. Преобразование не должно приводить к потере точности.
float x; uint64_t y; (float)x == (float)y
достаточно для обеспечения того, чтобы значения были эквивалентны? Будет ли это сравнение когда-либо правдой?Для целей данной спецификации все числовые представления
для того же числового значения эквивалентны. Это означает, что
кодировщик может кодировать значение с плавающей запятой 0.0 как целое число 0.
Это, однако, также означает, что приложение, которое ожидает найти
только целочисленные значения могут найти значения с плавающей точкой, если кодер
решает, что это желательно, например, когда значение с плавающей запятой
более компактный, чем 64-разрядное целое число.
Конечно, есть цифры, для которых это правда:
2 ^ 33 может быть прекрасно представлено как число с плавающей запятой, но явно не может быть представлено как 32-разрядное целое число. Следующий код должен работать как положено:
bool representable_as_float(int64_t value) {
float repr = value;
return repr >= -0x1.0p63 && repr < 0x1.0p63 && (int64_t)repr == value;
}
Однако важно отметить, что мы в основном выполняем значение (int64_t) (float), а не наоборот — нас интересует, теряет ли приведение типа float какую-либо точность.
Важно проверить, меньше ли значение repr, чем максимальное значение int64_t, поскольку в противном случае мы можем вызвать неопределенное поведение, поскольку приведение к типу float может округляться до следующего большего числа (которое может быть больше максимального значения, возможного в int64_t. ). (Спасибо @tmyklebu за указание на это).
Два образца:
// powers of 2 can easily be represented
assert(representable_as_float(((int64_t)1) << 33));
// Other numbers not so much:
assert(!representable_as_float(std::numeric_limits<int64_t>::max()));
Следующее основано на Метод Юлии для сравнения чисел с плавающей точкой и целых чисел. Это не требует доступа к 80-битной long double
s или исключения с плавающей запятой, и должны работать в любом режиме округления. Я считаю, что это должно работать для любого C float
тип (IEEE754 или нет), и не вызывает никакого неопределенного поведения.
ОБНОВЛЕНИЕ: технически это предполагает двоичный float
формат, и что float
Размер экспоненты достаточно велик, чтобы представлять 264Это верно для стандартного двоичного кода IEEE754 (о котором вы говорите в своем вопросе), но не, скажем, для двоичного16.
#include <stdio.h>
#include <stdint.h>
int cmp_flt_uint64(float x,uint64_t y) {
return (x == (float)y) && (x != 0x1p64f) && ((uint64_t)x == y);
}
int main() {
float x = 0x1p64f;
uint64_t y = 0xffffffffffffffff;
if (cmp_flt_uint64(x,y))
printf("true\n");
else
printf("false\n");
;
}
Логика здесь следующая:
x
неотрицательное целое число в интервале [0,264].x
(и поэтому (float)y
) не 264: если это так, то y
не может быть представлен точно float
и поэтому сравнение неверно.x
может быть точно преобразован в uint64_t
и так мы снимаем и сравниваем.Нет нужно сравнивать (long double)x == (long double)y
на архитектуре, где мантисса длинного двойника может содержать 63 бита. Это связано с тем, что некоторые большие длинные длинные целые числа теряют точность, когда вы конвертируете их в число с плавающей запятой и сравниваете их как равные неэквивалентным значениям с плавающей запятой, но если вы конвертируете в длинные двойные числа, они не потеряют точность в этой архитектуре.
Следующая программа демонстрирует это поведение при компиляции с gcc -std=c99 -mssse3 -mfpmath=sse
на x86, поскольку в этих настройках используются достаточно широкие двойные числа, но не допускается неявное использование типов с более высокой точностью в вычислениях:
#include <assert.h>
#include <stdint.h>
const int64_t x = (1ULL<<62) - 1ULL;
const float y = (float)(1ULL<<62);
// The mantissa is not wide enough to store
// 63 bits of precision.
int main(void)
{
assert ((float)x == (float)y);
assert ((long double)x != (long double)y);
return 0;
}
Редактировать: Если у вас недостаточно широких длинных пар, может сработать следующее:
feclearexcept(FE_ALL_EXCEPT);
x == y;
ftestexcept(FE_INEXACT);
Я думаю, хотя я могу ошибаться, что реализация может округлять x во время преобразования таким образом, что теряет точность.
Еще одна стратегия, которая может работать, это сравнить
extern uint64_t x;
extern float y;
const float z = (float)x;
y == z && (uint64_t)z == x;
Это должно отразить потери точности из-за ошибки округления, но это может привести к неопределенному поведению, если преобразование в z округляется в большую сторону. Это будет работать, если при преобразовании x в z будет установлено округление до нуля.