Подтвердить что ты не робот

Преобразование плавающей запятой 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, которые, по-видимому, чревато несогласованностью. Я еще не нашел пути решения этих проблем.

4b9b3361

Ответ 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 кажется лучшим вариантом.