Безопасно ли преобразовывать байты в число с плавающей точкой или это может привести к неопределенному поведению?
Существуют ли последовательности байтов, которые при преобразовании в 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.