Преобразование IEEE 754 с плавающей точкой в ​​Haskell Word32 / 64 в и из Haskell Float / Double - PullRequest
38 голосов
/ 08 августа 2011

Вопрос

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

Ответы [ 4 ]

18 голосов
/ 10 августа 2011

Саймон Марлоу упоминает другой подход в ошибка 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 и трех примочек (newByteArray#, writeWord32Array#, readFloatArray#).

Я не уверен, на что похожа производительность по сравнению с методом сортировки FFI, но просто для удовольствия я сравнил ядро ​​, сгенерированное обеими опциями .

Выполнение сортировки FFI в этом отношении немного сложнее. Он вызывает unsafeDupablePerformIO и 7 прайм-пиков (noDuplicate#, newAlignedPinnedByteArray#, unsafeFreezeByteArray#, byteArrayContents#, writeWord32OffAddr#, readFloatOffAddr#, touch#).

Я только начал изучать, как анализировать ядро, возможно, кто-то с большим опытом может прокомментировать стоимость этих операций?

18 голосов
/ 09 августа 2011

Все современные процессоры используют IEEE754 для плавающей запятой, и это вряд ли изменится в течение нашей жизни. Так что не беспокойтесь о том, что код делает такое предположение.

Вы совершенно определенно не можете использовать unsafeCoerce или unsafeCoerce# для преобразования между целочисленными типами и типами с плавающей запятой, поскольку это может вызвать как сбои компиляции, так и сбои во время выполнения. Подробнее см. ошибка GHC 2209 .

До тех пор, пока Ошибка GHC 4092 , которая устраняет необходимость принуждения int↔fp, исправлена, единственный безопасный и надежный подход - через FFI.

7 голосов
/ 09 августа 2011

Я автор data-binary-ieee754. В какой-то момент он использовал каждый из трех вариантов.

encodeFloat и decodeFloat работают достаточно хорошо для большинства случаев, но дополнительный код, необходимый для их использования, добавляет колоссальные издержки. Они плохо реагируют на NaN или Infinity, поэтому для любых приведений на их основе требуются определенные допущения, специфичные для GHC.

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

Код FFI до сих пор был самым надежным и имел достойную производительность. Затраты на распределение не так плохи, как кажется, вероятно, из-за модели памяти GHC. И на самом деле не зависит от внутреннего формата чисел с плавающей запятой, просто от поведения экземпляра Storable. Компилятор может использовать любое желаемое представление, если Storable - это IEEE-754. В любом случае, GHC использует IEEE-754 для внутреннего использования, и я больше не беспокоюсь о компиляторах не-GHC, так что это спорный вопрос.

До тех пор, пока разработчики GHC не сочтут целесообразным даровать нам неснятые слова фиксированной ширины со связанными функциями преобразования, FFI кажется лучшим вариантом.

6 голосов
/ 09 августа 2011

Я бы использовал метод FFI для конвертации.Но обязательно используйте выравнивание при выделении памяти, чтобы получить память, приемлемую для загрузки / хранения как для числа с плавающей запятой, так и для целого числа.Вам также следует добавить некоторое утверждение о том, что размеры с плавающей запятой и слова одинаковы, чтобы вы могли определить, если что-то пойдет не так.

Если выделение памяти заставляет вас съеживаться, вам не следует использовать Haskell.:)

...