acf*_*zer 38 floating-point haskell ghc ieee-754
在Haskell中,base库和Hackage包提供了几种将二进制IEEE-754浮点数据转换为提升Float和Double类型的方法.但是,这些方法的准确性,性能和可移植性尚不清楚.
对于旨在(跨)平台(反)序列化二进制格式的GHC目标库,处理IEEE-754浮点数据的最佳方法是什么?
这些是我在现有库和在线资源中遇到的方法.
这是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
Run Code Online (Sandbox Code Playgroud)
在我的机器上这是有效的,但我畏缩看到分配只是为了完成强制.此外,虽然这个解决方案并不是唯一的,但这里隐含的假设是IEEE-754实际上是内存中的表示.包装附带的测试给它"在我的机器上工作"的批准印章,但这并不理想.
unsafeCoerce使用内存中IEEE-754表示的相同隐含假设,以下代码也可以获得"在我的机器上工作"的封条:
toFloat :: Word32 -> Float
toFloat = unsafeCoerce
Run Code Online (Sandbox Code Playgroud)
这样做的好处是不像上面的方法那样执行显式分配,但是文档说"你有责任确保旧的和新的类型具有相同的内部表示".这个隐含的假设仍然在做所有的工作,在处理提升的类型时更加费劲.
unsafeCoerce#扩展可能被视为"便携"的限制:
toFloat :: Word -> Float
toFloat (W# w) = F# (unsafeCoerce# w)
Run Code Online (Sandbox Code Playgroud)
这似乎有效,但似乎并不实用,因为它仅限于GHC.Exts类型.绕过提升的类型很好,但这就是所有可以说的.
encodeFloat 和 decodeFloat这种方法具有绕过unsafe名称中的任何东西的良好属性,但似乎没有得到IEEE-754非常正确.一个以前的SO回答一个类似的问题提供了一个简洁的方法,而ieee754-parser包中使用较普遍的办法有利于被弃用之前data-binary-ieee754.
有相当多的上诉,以具有大约需要底层表示没有隐含假设代码,但这些解决方案依赖于encodeFloat和decodeFloat,这显然与矛盾充满.我还没有找到解决这些问题的方法.
Bry*_*van 18
所有现代CPU都使用IEEE754作为浮点,这似乎不太可能在我们的生命周期内改变.所以不要担心代码做出这个假设.
您绝对不能自由使用unsafeCoerce或unsafeCoerce#在积分和浮点类型之间进行转换,因为这会导致编译失败和运行时崩溃.有关详细信息,请参阅GHC错误2209.
直到解决int↔fp强制需求的GHC bug 4092是固定的,唯一安全可靠的方法是通过FFI.
Jac*_*ley 18
Simon Marlow在GHC bug 2209中提到了另一种方法(也与Bryan O'Sullivan的回答有关)
顺便说一句,你可以使用castSTUArray达到预期的效果(这是我们在GHC中的方式).
我在我的一些库中使用了这个选项,以避免unsafePerformIOFFI编组方法所需.
{-# 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
Run Code Online (Sandbox Code Playgroud)
我内联了cast函数,因为这样做会导致GHC产生更紧密的核心.内联后,wordToFloat转换为一个呼叫runSTRep和三个primops(newByteArray#,writeWord32Array#,readFloatArray#).
我不确定与FFI编组方法相比有什么性能,但只是为了好玩,我比较了两个选项产生的核心.
在这方面,进行FFI编组操作要复杂得多.它要求unsafeDupablePerformIO和7个primops( ,noDuplicate#,newAlignedPinnedByteArray#,unsafeFreezeByteArray#,byteArrayContents#,writeWord32OffAddr#,).readFloatOffAddr#touch#
我刚刚开始学习如何分析核心,也许有经验的人可以评论这些操作的成本?
我是作者data-binary-ieee754.它在某些时候使用了三个选项中的每一个.
encodeFloat并且decodeFloat在大多数情况下工作得很好,但使用它们所需的附件代码会增加巨大的开销.它们对NaN或者反应不好Infinity,因此基于它们的任何演员都需要一些GHC特定的假设.
unsafeCoerce是为了获得更好的性能而尝试替换.这真的很快,但其他图书馆有重大问题的报道让我最终决定避免它.
到目前为止,FFI代码是最可靠的,并且具有良好的性能.分配的开销并不像看起来那么糟糕,可能是由于GHC内存模型.它实际上并不依赖于浮点数的内部格式,而只取决于Storable实例的行为.只要Storable是IEEE-754 ,编译器就可以使用它想要的任何表示.无论如何GHC内部使用IEEE-754,我不再担心非GHC编译器,所以这是一个有争议的问题.
直到GHC开发人员认为适合赋予我们未固定的固定宽度字,并具有相关的转换功能,FFI似乎是最好的选择.
我使用FFI方法进行转换.但是一定要在分配内存时使用对齐,这样就可以获得浮点数和整数的加载/存储可接受的内存.您还应该对浮点数和单词的大小进行一些断言,以便您可以检测是否出现任何问题.
如果分配内存会让你感到畏缩,那么就不应该使用Haskell.:)