有效地将64位Double转换为ByteString

Sal*_*Sal 6 haskell casting bytestring

我编写了一个函数将64位Double转换为ByteString(架构/类型安全性并不是真正的问题 - 我们现在假设Double是64位Word).虽然下面的函数运行良好,但我想知道是否有更快的方法将Double转换为ByteString.在下面的代码中,有一个解压缩Word64到Word8列表,然后反向(使它成为小端格式),然后打包到ByteString.代码如下:

{-# LANGUAGE MagicHash #-}
import GHC.Prim
import GHC.Types
import GHC.Word
import Data.Bits (shiftR)
import Data.ByteString (pack, unpack)
import Data.ByteString.Internal (ByteString)
import Text.Printf (printf)

encodeDouble :: Double -> ByteString
encodeDouble (D# x) = pack $ reverse $ unpack64 $ W64# (unsafeCoerce# x)

unpack64 :: Word64 -> [Word8]
unpack64 x = map (fromIntegral.(shiftR x)) [56,48..0]

-- function to convert list of bytestring into hex digits - for debugging
bprint :: ByteString -> String
bprint x = ("0x" ++ ) $ foldl (++) "" $ fmap (printf "%02x") $ unpack x

main = putStrLn $ bprint $ encodeDouble 7234.4
Run Code Online (Sandbox Code Playgroud)

Mac x86上的GHCi示例输出:

*Main> bprint $ encodeDouble 7234.4
"0x666666666642bc40"
Run Code Online (Sandbox Code Playgroud)

虽然代码似乎运行良好,但我计划在通过IPC发送之前使用它将很多Double值编码为ByteString.所以,如果有的话,我会很高兴能让它变得更快.

在我看来,double必须解压缩到Word8中,然后打包到ByteString中.因此,可能是整体算法,无法改进.但是,使用更高效的解包/打包功能可能会有所不同,如果有的话.

EDIT1: 我刚刚在Mac上发现了另一个复杂问题(GHC 7.0.3) - 由于这个错误,上面的代码无法在GHC中编译 - 我到目前为止在GHCi中进行了测试:

$ ghc -O --make t.hs
[1 of 1] Compiling Main             ( t.hs, t.o )

/var/folders/_q/33htc59519b3xq7y6xv100z40000gp/T/ghc6976_0/ghc6976_0.s:285:0:
    suffix or operands invalid for `movsd'

/var/folders/_q/33htc59519b3xq7y6xv100z40000gp/T/ghc6976_0/ghc6976_0.s:304:0:
    suffix or operands invalid for `movsd'
Run Code Online (Sandbox Code Playgroud)

因此,看起来我必须依赖FFI(谷物/数据二进制-ieee754包)直到修复此错误,或者直到找到解决方法.看起来像GHC Ticket 4092.如果这是一个新的错误或不同的错误,请纠正我.现在,我无法编译它:(

EDIT2: 更新代码以使用unsafeCoerce修复了编译问题.以下代码使用Criterion基准:

{-# LANGUAGE MagicHash #-}
import GHC.Prim
import GHC.Types
import GHC.Word
import Data.Bits (shiftR)
import Data.ByteString (pack, unpack)
import Data.ByteString.Internal (ByteString)
import Text.Printf (printf)
import Unsafe.Coerce
import Criterion.Main

--encodeDouble :: Double -> ByteString
encodeDouble  x = pack $ reverse $ unpack64 $ unsafeCoerce x

unpack64 :: Word64 -> [Word8]
unpack64 x = map (fromIntegral.(shiftR x)) [56,48..0]

main = defaultMain [
        bgroup "encodeDouble" [
          bench "78901.234"  $ whnf encodeDouble 78901.234
          , bench "789.01" $ whnf encodeDouble 789.01
          ]
       ]
Run Code Online (Sandbox Code Playgroud)

标准输出(截断):

estimating cost of a clock call...
mean is 46.09080 ns (36 iterations)

benchmarking encodeDouble/78901.234
mean: 218.8732 ns, lb 218.4946 ns, ub 219.3389 ns, ci 0.950
std dev: 2.134809 ns, lb 1.757455 ns, ub 2.568828 ns, ci 0.950

benchmarking encodeDouble/789.01
mean: 219.5382 ns, lb 219.0744 ns, ub 220.1296 ns, ci 0.950
std dev: 2.675674 ns, lb 2.197591 ns, ub 3.451464 ns, ci 0.950
Run Code Online (Sandbox Code Playgroud)

进一步分析,大多数瓶颈似乎都在unpack64中.强制需要约6ns.unpack64需要~192ns.将word64解压缩为word8列表在这里非常昂贵.

acf*_*zer 5

我最近向 中添加了对 IEEE-754 浮点数的支持cereal,您可以binary在 中找到类似的函数data-binary-ieee754。下面是一个使用cereal版本来往返pia 的示例ByteString

\n\n
Prelude Data.Serialize> runGet getFloat64be $ runPut $ putFloat64be pi\nRight 3.141592653589793\n
Run Code Online (Sandbox Code Playgroud)\n\n

它使用 ST 数组的技巧来快速完成转换;有关更多详细信息,请参阅之前的问题

\n\n

更新:哦,我应该知道如何使用我为库贡献的调用......

\n\n

更新x2:关于编译失败,我认为这不属于错误。

\n\n

我没有仔细查看为该特定代码生成的程序集,但指令的操作数movsd正在变得混乱。来自Intel x86 手册的 \xc2\xa711.4.1.1 :

\n\n
\n

MOVSD(移动标量双精度浮点)将 64 位双精度浮点操作数从内存传输到 XMM 寄存器的低四字,反之亦然,或者在 XMM 寄存器之间传输。

\n
\n\n

在未优化的代码中,您有诸如 之类的精细指令movsd LnTH(%rip),%xmm0,但在-O代码中,您会看到诸如 之类的内容movsd Ln2cJ(%rip),%rax,其中%rax是通用寄存器,而不是 XMM 寄存器。

\n\n

优化器可能会根据所涉及的数据类型对需要在寄存器之间移动的数据表示做出假设。unsafeCoerce和朋友们使这些假设无效,因此当指令选择器认为它正在为 a 选择正确的操作时D#,它实际上会发出试图填充D#aW64#很适合的代码。

\n\n

由于处理这个问题需要优化器放弃许多让它在正常情况下发出更好的代码的假设,所以我倾向于说这不是一个错误,而是一个关于为什么unsafe函数带有买者自负警告的好故事。

\n


Dan*_*her 2

unsafeCoerce#请注意,文档中说,这里使用是危险的

将未装箱类型转换为相同大小的另一个未装箱类型(但不是浮点类型和整型类型之间的强制转换

unsafeCreate关于速度,避免中间列表并通过from直接写入内存可能会更快Data.ByteString.Internal