首页 文章

将Haskell Word32 / 64中的IEEE 754浮点转换为Haskell Float / Double

提问于
浏览
37

问题

在Haskell中, base 库和Hackage包提供了几种将二进制IEEE-754浮点数据转换为提升的 FloatDouble 类型的方法 . 但是,这些方法的准确性,性能和可移植性尚不清楚 .

对于旨在(反)序列化跨平台二进制格式的GHC目标库,处理IEEE-754浮点数据的最佳方法是什么?

方法

这些是我在现有库和在线资源中遇到的方法 .

FFI Marshaling

这是data-binary-ieee754包使用的方法 . 由于 FloatDoubleWord32Word64Storable 的每个实例,因此可以将源类型的值 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

这样做的好处是不像上面的方法那样执行显式分配,但documentation表示"it is your responsibility to ensure that the old and new types have identical internal representations" . 这个隐含的假设仍然在做所有的工作,在处理提升的类型时更加费劲 .

unsafeCoerce#

扩展可能被视为“便携”的限制:

toFloat :: Word -> Float
toFloat (W# w) = F# (unsafeCoerce# w)

这似乎有效,但不限于GHC.Exts类型 . 关于所有可以说的话,它都是's nice to bypass the lifted types, but that' .

encodeFloat和decodeFloat

这种方法具有绕过名称中 unsafe 的任何东西的良好属性,但似乎没有使IEEE-754非常正确 . 类似问题的previous SO answer提供了一种简洁的方法,ieee754-parser包在被弃用之前使用了更为通用的方法,而有利于 data-binary-ieee754 .

对于不需要隐含关于底层表示的假设的代码有很大的吸引力,但这些解决方案依赖于 encodeFloatdecodeFloat ,这显然是fraught with inconsistencies . 我还没有找到解决这些问题的方法 .

4 回答

  • 7

    Simon Marlow在_831777中提到了另一种方法(也与Bryan O _831778的答案有关)

    顺便说一句,你可以使用castSTUArray达到预期的效果(这是我们在GHC中的方式) .

    我在我的一些库中使用了这个选项,以避免FFI编组方法所需的 unsafePerformIO .

    {-# 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的调用和三个primopsnewByteArray#writeWord32Array#readFloatArray# ) .

    与FFI编组方法相比,我不确定性能是什么样的,但为了好玩,我比较了核心generated by both options .

    在这方面,做FFI编组操作要复杂得多 . 它调用unsafeDupablePerformIO和7个primops( noDuplicate#newAlignedPinnedByteArray#unsafeFreezeByteArray#byteArrayContents#writeWord32OffAddr#readFloatOffAddr#touch# ) .

    我刚刚开始学习如何分析核心,也许有经验的人可以评论这些操作的成本?

  • 6

    所有现代CPU都使用IEEE754作为浮点,这似乎不太可能在我们的生命周期内改变 . 所以不要担心代码做出这个假设 .

    您绝对不能自由地使用 unsafeCoerceunsafeCoerce# 来在积分和浮点类型之间进行转换,因为这会导致编译失败和运行时崩溃 . 有关详细信息,请参阅GHC bug 2209 .

    直到GHC bug 4092,解决了对强制执行的需求,是唯一安全可靠的方法是通过FFI .

  • 18

    我是 data-binary-ieee754 的作者 . 它在某些时候使用了三个选项中的每一个 .

    encodeFloatdecodeFloat 在大多数情况下运行良好,但使用它们所需的附件代码会增加巨大的开销 . 它们对 NaNInfinity 反应不佳,因此任何基于它们的演员都需要一些GHC特定的假设 .

    unsafeCoerce 是一次尝试替换,以获得更好的性能 . 这真的很快,但其他图书馆有重大问题的报道让我最终决定避免它 .

    到目前为止,FFI代码是最可靠的,并且具有良好的性能 . 分配的开销并不像看起来那么糟糕,可能是由于GHC内存模型 . 它实际上并不依赖于浮点数的内部格式,而只取决于 Storable 实例的行为 . 只要 Storable 是IEEE-754,编译器就可以使用它想要的任何表示 . GHC无论如何都在内部使用IEEE-754,我也没有任何意义点 .

    直到GHC开发人员认为适合赋予我们未固定的固定宽度字,并具有相关的转换功能,FFI似乎是最好的选择 .

  • 18

    我使用FFI方法进行转换 . 但是一定要在分配内存时使用对齐,这样就可以获得浮点数和整数的加载/存储都可以接受的内存 . 你还应该放一些关于float和word的大小相同的断言,这样你就可以检测出是否有什么问题 .

    如果分配内存让你感到畏缩,你不应该使用Haskell . :)

相关问题