Сравнение 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-битное целое число.


person Alexander Shishenko    schedule 27.09.2015    source источник
comment
What 32-bit float numbers can be bigger than 64-bit integer and numerically equivalent with them? Нет: по определению число X равное Y не может быть больше Y.   -  person Igor Tandetnik    schedule 27.09.2015
comment
Пока 32-битное число с плавающей запятой является целым числом, вы не потеряете точность при преобразовании его в 64-битное целое число. Но если исходное число с плавающей запятой не является целым числом, вы потеряете точность.   -  person Logicrat    schedule 27.09.2015
comment
Конечно, такое сравнение может быть верным. 1.0f == float(1ull)   -  person Igor Tandetnik    schedule 27.09.2015
comment
Вопрос имеет смысл только в том случае, если речь идет о том, какие 64-битные целые числа могут быть представлены как числа с плавающей запятой без потери точности. Это также то, что говорится в первом абзаце, первый пункт довольно сбивает с толку. И, очевидно, существует довольно много чисел, для которых это свойство верно (любая степень двойки больше 2 ^ 32, но меньше 2 ^ 64 для единицы).   -  person Voo    schedule 27.09.2015
comment
Проверка, которая имеет значение, заключается в следующем. Если вы возьмете исходное 64-битное целое число, преобразуете его в число с плавающей запятой, затем преобразуете это число с плавающей запятой обратно в целое число и получите исходное значение, тогда вы сможете передать число с плавающей запятой вместо целого числа; вы можете быть уверены, что другая сторона сможет восстановить исходное целое число (потому что вы только что проверили его сами).   -  person Igor Tandetnik    schedule 27.09.2015
comment
почему вы думаете, что соответствующие 64-битные числа являются целыми числами?   -  person Walter    schedule 27.09.2015
comment
Отвечает ли это на ваш вопрос? Как правильно сравнивать целое число и число с плавающей запятой стоимость в баллах?   -  person phuclv    schedule 19.11.2019


Ответы (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)value, а не наоборот - нас интересует, теряет ли точность приведение к 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())); 
person Voo    schedule 27.09.2015
comment
Разве вы не получаете UB, делая, скажем, (int64_t)(float)0x7fffffffffffffffLL? Здесь преобразование в число с плавающей запятой округляется в большую сторону, поэтому преобразование в число int64_t будет переполняться, то есть UB. - person tmyklebu; 27.09.2015
comment
@tmyklebu Справедливое замечание. Преобразование из int -> float всегда безопасно (afaics?), а другое — нет. Что делает все это более интересным. - person Voo; 27.09.2015
comment
Хм... значит, это может привести к НБ, не так ли? - person Alexander Shishenko; 27.09.2015
comment
Я думаю, вам нужно проверить, является ли repr >= -0x1.0p63 && repr < 0x1.0p63 перед преобразованием в int64_t. - person tmyklebu; 28.09.2015

Следующее основано на методе Джулии для сравнения чисел с плавающей запятой и целых чисел< /а>. Это не требует доступа к 80-битным long doubles или исключениям с плавающей запятой и должно работать в любом режиме округления. Я считаю, что это должно работать для любого типа C float (IEEE754 или нет) и не вызывать неопределенного поведения.

ОБНОВЛЕНИЕ: технически это предполагает двоичный формат float и что размер экспоненты float достаточно велик, чтобы представлять 264: это, безусловно, верно для стандартного двоичного файла IEEE75432 (на который вы ссылаетесь в своем вопросе), но не, скажем, двоичный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, поэтому мы приводим и сравниваем.
person Simon Byrne    schedule 28.09.2015

Нет, вам нужно сравнить (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.

person Davislor    schedule 27.09.2015
comment
Я не уверен, почему за меня проголосовали, но, возможно, пример кода, демонстрирующий такое поведение, изменит ваше мнение? - person Davislor; 27.09.2015
comment
Я не проголосовал против - я фактически проголосовал за новую версию - но да, пример - это то, что делает ваш ответ ценным. Есть несколько приемов для сравнения целого числа и значения с плавающей запятой без типа long double, которые могут точно представлять все значения каждого исходного типа. gynvael.coldwind.pl/?id=535 twitter.com/spun_off/status/467929922259144704 - person Pascal Cuoq; 27.09.2015
comment
Ваш совет внизу проверяет ftrunc(x) == x, но x является целым числом в ваших примерах (и я не уверен, что он работает, даже если роли x и y поменялись местами, например, float x = 0x1.0p62, int64_t y = 0x3fffffffffffffff). - person Pascal Cuoq; 27.09.2015
comment
Упс, этот кусочек внизу был ответом на другой вопрос. - person Davislor; 27.09.2015
comment
Ну это немного оффтоп. Я хочу преобразовать uint64_t в float и сравнить его с исходным uint64_t, чтобы проверить, могу ли я упаковать число, используя 32 бита вместо 64, без потери исходного числового значения. - person Alexander Shishenko; 27.09.2015
comment
@Pascal Cuoc Спасибо за пример кода! Я не уверен, насколько это портативно, но я знаю, что long double не всегда будет работать для этого везде. - person Davislor; 27.09.2015
comment
@Alexander Shishenko: Сравнение с результатом преобразования туда и обратно работает для этого. - person Davislor; 27.09.2015
comment
@Lorehead Я вижу, пример с использованием feclearexcept/ftestexcept очень интересен. Я скоро проверю это в своем коде. - person Alexander Shishenko; 27.09.2015
comment
И добавили стратегию туда-обратно. - person Davislor; 27.09.2015
comment
@Lorehead В том, что вы называете «стратегией кругового пути», (uint64_t)z может вызвать неопределенное поведение, например. для x установлено значение 0xffffffffffffffff, в результате чего z становится 0x1.0p64. - person Pascal Cuoq; 27.09.2015
comment
Вы по-прежнему можете активировать UB в решении туда и обратно, выбрав float y = 0x1.0p64; и uint64_t x = 0xffffffffffffffff; - person Pascal Cuoq; 27.09.2015
comment
@Pascal Cuoc: Вы правы. Мне нужно настроить библиотеку на усечение при преобразовании. - person Davislor; 27.09.2015