Сравнение uint64_t и float для числовой эквивалентности

Я пишу протокол, который использует RFC 7049 как его двоичное представление. Стандарт гласит, что протокол может использовать 32-битное представление чисел с плавающей запятой, если их числовое значение эквивалентно соответствующим 64-битным числам. Преобразование не должно приводить к потере точности.

  • Какие 32-разрядные числа с плавающей запятой могут быть больше 64-разрядного целого числа и численно эквивалентны им?
  • Сравнивает float x; uint64_t y; (float)x == (float)y достаточно для обеспечения того, чтобы значения были эквивалентны? Будет ли это сравнение когда-либо правдой?

RFC 7049 §3.6. чисел

Для целей данной спецификации все числовые представления
для того же числового значения эквивалентны. Это означает, что
кодировщик может кодировать значение с плавающей запятой 0.0 как целое число 0.
Это, однако, также означает, что приложение, которое ожидает найти
только целочисленные значения могут найти значения с плавающей точкой, если кодер
решает, что это желательно, например, когда значение с плавающей запятой
более компактный, чем 64-разрядное целое число.

3

Решение

Конечно, есть цифры, для которых это правда:

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()));
1

Другие решения

Следующее основано на Метод Юлии для сравнения чисел с плавающей точкой и целых чисел. Это не требует доступа к 80-битной long doubles или исключения с плавающей запятой, и должны работать в любом режиме округления. Я считаю, что это должно работать для любого 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и так мы снимаем и сравниваем.
1

Нет нужно сравнивать (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 будет установлено округление до нуля.

-1
По вопросам рекламы [email protected]