Преобразование плавающей запятой IEEE 754 в Haskell Word32/64 в и из Haskell Float/Double
Вопрос
В Haskell библиотеки base
и пакеты Hackage предоставляют несколько способов преобразования двоичных данных с плавающей запятой IEEE-754 в и с поднятых типов Float
и Double
. Однако точность, производительность и переносимость этих методов неясны.
Для библиотеки, ориентированной на GHC, предназначенной для (де) сериализации бинарного формата на разных платформах, наилучший подход для обработки данных с плавающей точкой IEEE-754?
Подходы
Это методы, с которыми я столкнулся в существующих библиотеках и онлайн-ресурсах.
FFI Marshaling
Это подход, используемый data-binary-ieee754
. Поскольку Float
, Double
, Word32
и Word64
- это каждый экземпляр Storable
, можно указать poke
значение типа источника во внешний буфер, а затем peek
значение целевой Тип:
toFloat :: (F.Storable word, F.Storable float) => word -> float
toFloat word = F.unsafePerformIO $ F.alloca $ \buf -> do
F.poke (F.castPtr buf) word
F.peek buf
На моей машине это работает, но я сжимаю, чтобы видеть, что распределение выполняется только для того, чтобы добиться принуждения. Кроме того, хотя это и не является уникальным для этого решения, здесь подразумевается предположение о том, что IEEE-754 фактически является представлением в памяти. Тестирование, сопровождающее упаковку, дает одобрение печати "работает на моей машине", но это не идеально.
unsafeCoerce
При таком же неявном допущении в представлении IEEE-754 в памяти следующий код также получает печать "работ на моей машине":
toFloat :: Word32 -> Float
toFloat = unsafeCoerce
Это позволяет не выполнять явное распределение, как описанный выше подход, но документация говорит: "Вы несете ответственность за то, чтобы старые и новые типы имеют идентичные внутренние представления". Это подразумеваемое предположение все еще выполняет всю работу и еще более напряжено при работе с отмененными типами.
unsafeCoerce#
Растяжение пределов того, что можно считать "переносимым":
toFloat :: Word -> Float
toFloat (W# w) = F# (unsafeCoerce# w)
Это, похоже, работает, но не кажется практичным вообще, поскольку оно ограничено типами GHC.Exts
. Приятно обходить поднятые типы, но это обо всем, что можно сказать.
encodeFloat
и decodeFloat
Этот подход имеет приятное свойство обойти что-либо с unsafe
в названии, но, похоже, не совсем корректен для IEEE-754. A предыдущий SO ответ по аналогичному вопросу предлагает краткий подход, а ieee754-parser
используется более общий подход, прежде чем устаревать в пользу data-binary-ieee754
.
Здесь довольно немного привлекает код, который не нуждается в неявных предположениях о базовом представлении, но эти решения полагаются на encodeFloat
и decodeFloat
, которые, по-видимому, чревато несогласованностью. Я еще не нашел пути решения этих проблем.
Ответы
Ответ 1
Саймон Марлоу упоминает о другом подходе в ошибке GHC 2209 (также связанном с ответом Брайана О'Салливана)
Вы можете добиться желаемого эффекта, используя castSTUArray, кстати (так мы делаем это в GHC).
Я использовал эту опцию в некоторых моих библиотеках, чтобы избежать unsafePerformIO
, необходимого для метода сортировки FFI.
{-# LANGUAGE FlexibleContexts #-}
import Data.Word (Word32, Word64)
import Data.Array.ST (newArray, castSTUArray, readArray, MArray, STUArray)
import GHC.ST (runST, ST)
wordToFloat :: Word32 -> Float
wordToFloat x = runST (cast x)
floatToWord :: Float -> Word32
floatToWord x = runST (cast x)
wordToDouble :: Word64 -> Double
wordToDouble x = runST (cast x)
doubleToWord :: Double -> Word64
doubleToWord x = runST (cast x)
{-# INLINE cast #-}
cast :: (MArray (STUArray s) a (ST s),
MArray (STUArray s) b (ST s)) => a -> ST s b
cast x = newArray (0 :: Int, 0) x >>= castSTUArray >>= flip readArray 0
Я включил функцию трансляции, потому что это приводит к тому, что GHC генерирует гораздо более жесткое ядро. После вставки wordToFloat
переводится на вызов runSTRep и три primops (newByteArray#
, writeWord32Array#
, readFloatArray#
).
Я не уверен, что такое производительность по сравнению с методом Marshalling FFI, но просто для удовольствия я сравнивал ядро сгенерированное обоими параметрами.
Выполнение FFI marshalling в этом отношении является более сложным. Он вызывает unsafeDupablePerformIO и 7 примитивов (noDuplicate#
, newAlignedPinnedByteArray#
, unsafeFreezeByteArray#
, byteArrayContents#
, writeWord32OffAddr#
, readFloatOffAddr#
, touch#
).
Я только начал изучать, как анализировать ядро, возможно, кто-то с большим опытом может прокомментировать стоимость этих операций?
Ответ 2
Все современные процессоры используют IEEE754 для плавающей запятой, и это вряд ли изменится в течение нашей жизни. Поэтому не беспокойтесь о том, что код делает это предположение.
Вы, безусловно, не можете использовать unsafeCoerce
или unsafeCoerce#
для преобразования между целыми и плавающими типами, поскольку это может привести к сбоям компиляции и сбоям во время выполнения. Подробнее см. GHC ошибка 2209.
До ошибка GHC 4092, которая устраняет необходимость в принудительном принуждении, исправлена, единственный безопасный и надежный подход - через FFI.
Ответ 3
Я бы использовал метод FFI для преобразования. Но не забудьте использовать выравнивание при распределении памяти, чтобы получить память, приемлемую для загрузки/хранения как числа с плавающей запятой, так и целого числа. Вы также должны указать некоторые утверждения о том, что размеры float и word совпадают, поэтому вы можете обнаружить, что что-то пошло не так.
Если выделение памяти заставляет вас съеживаться, вы не должны использовать Haskell.:)
Ответ 4
Я автор data-binary-ieee754
. В какой-то момент он использовал каждый из трех вариантов.
encodeFloat
и decodeFloat
работают достаточно хорошо для большинства случаев, но дополнительный код, необходимый для их использования, добавляет огромные накладные расходы. Они не очень хорошо реагируют на NaN
или Infinity
, поэтому некоторые предположения, связанные с GHC, необходимы для любых бросков, основанных на них.
unsafeCoerce
была попыткой замены, чтобы получить лучшую производительность. Это было очень быстро, но сообщения о других библиотеках, имеющие значительные проблемы, заставили меня в конечном итоге принять решение об этом.
Код FFI до сих пор был самым надежным и имел достойную производительность. Накладные расходы не так плохи, как кажется, вероятно, из-за модели памяти GHC. И это фактически не зависит от внутреннего формата float, а только от поведения экземпляра Storable
. Компилятор может использовать любое представление, которое он хочет, если Storable
- IEEE-754. В любом случае GHC использует IEEE-754, и я больше не беспокоюсь о компиляторах, отличных от GHC, поэтому это спорный вопрос.
До тех пор, пока разработчики GHC не увидят, что мы используем нефиксированные слова фиксированной ширины, с соответствующими функциями преобразования, FFI кажется лучшим вариантом.