Безопасно ли преобразовывать байты в число с плавающей точкой или это может привести к неопределенному поведению?

Существуют ли последовательности байтов, которые при преобразовании в f32 или же f64, производить неопределенное поведение в Rust? Я считаю неконечные значения, такие как NaN, Infinity и т. Д., Как допустимые значения с плавающей запятой.

Комментарии к этому ответу намекают на то, что могут быть некоторые проблемы с преобразованием плавающего числа из необработанных байтов.

1 ответ

Решение

Справочник Rust содержит хороший список ситуаций, в которых происходит неопределенное поведение. Из них наиболее тесно связанным с этим вопросом является следующее:

Неверные значения в примитивных типах, даже в частных полях / локальных:

  • Висячие / нулевые ссылки или поля
  • Значение, отличное от false (0) или true (1) в bool
  • Дискриминант в перечислении, не включенном в определение типа
  • Значение в символе, которое является суррогатом или выше char::MAX
  • Байтовые последовательности не-UTF-8 в строке

И все же типы с плавающей запятой не перечислены. Это потому, что любая битовая последовательность (32 бита для f32; 64 бита для f64) является допустимым состоянием для значения с плавающей точкой, в соответствии с типами IEEE 754-2008 binary32 и binary64 с плавающей точкой. Они могут быть ненормальными (другие классы равны нулю, субнормальными, бесконечными или не являются числами), но, тем не менее, действительны.

В конце концов, всегда должен быть другой путь transmute, В частности, byteorder Crate обеспечивает безопасный и интуитивно понятный способ чтения чисел из потока байтов.

use byteorder::{ByteOrder, LittleEndian}; // or NativeEndian

let bytes = [0x00u8, 0x00, 0x80, 0x7F];
let number = LittleEndian::read_f32(&bytes);
println!("{}", number);

Детская площадка


Хорошо, на самом деле существует очень специфический крайний случай, когда преобразование битов в число с плавающей запятой может привести к сигналу NaN, который в некоторых архитектурах и конфигурациях ЦП вызовет исключение низкого уровня. Смотрите обсуждение в ржавчине #39271 для деталей. В настоящее время известно, что материализация сигнальных NaN не является неопределенным поведением и что, если включены исключения с плавающей запятой, которые не являются по умолчанию, это вряд ли станет проблемой.

Уже реализованное решение команды библиотек Rust заключается в том, что преобразование в число с плавающей точкой является безопасным даже без какой-либо маскировки. Обоснование очень хорошо описано в документации для f32::from_bits:

В настоящее время это идентично transmute::<u32, f32>(v) на всех платформах. Оказывается, это невероятно портативно по двум причинам:

  • Float и Ints имеют одинаковый порядок байтов на всех поддерживаемых платформах.
  • IEEE-754 очень точно определяет битовую разметку поплавков.

Однако есть одна оговорка: до версии 2008 года IEEE-754, как интерпретировать бит сигнализации NaN, фактически не было указано. Большинство платформ (в частности, x86 и ARM) выбрали интерпретацию, которая в конечном итоге была стандартизирована в 2008 году, но некоторые - нет (особенно MIPS). В результате все сигнальные NaN на MIPS являются тихими NaN на x86, и наоборот.

Вместо того, чтобы пытаться сохранить кроссплатформенность сигнализации, эта реализация предпочитает сохранять точные биты. Это означает, что любые полезные данные, закодированные в NaN, будут сохранены, даже если результат этого метода будет отправлен по сети с компьютера с архитектурой x86 на компьютер MIPS.

Если результаты этого метода манипулируют только той же архитектурой, которая их произвела, то проблемы переносимости не возникает.

Если ввод не NaN, то нет проблем с переносимостью.

Если вас не волнует сигнализация (очень вероятно), тогда нет проблем с переносимостью.

Некоторые библиотеки синтаксического анализа / кодирования могут по-прежнему преобразовывать все виды NaN в гарантированно тихий NaN, поскольку этот вопрос был неопределенным некоторое время в истории Rust.

Другие вопросы по тегам