有效地将64位Double转换为ByteString

时间:2011-12-02 01:50:05

标签: haskell casting bytestring

我编写了一个将64位Double转换为ByteString的函数(架构/类型安全性并不是真正的问题 - 我们现在假设Double是64位字)。虽然下面的函数运行良好,但我想知道是否有更快的方法将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

Mac x86上的GHCi示例输出:

*Main> bprint $ encodeDouble 7234.4
"0x666666666642bc40"

虽然代码似乎运行良好,但我计划在通过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'

所以,看起来我必须依靠FFI(谷物/数据二进制-ieee754软件包),直到修复了这个bug,或者直到找到解决方法。看起来与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
          ]
       ]

标准输出(截断):

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

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

3 个答案:

答案 0 :(得分:4)

我最近添加了对cereal的IEEE-754浮点数的支持,您可以在data-binary-ieee754中找到binary的类似函数。以下是使用cereal版本将pi转发到ByteString并返回的示例:

Prelude Data.Serialize> runGet getFloat64be $ runPut $ putFloat64be pi
Right 3.141592653589793

它使用ST阵列技巧快速进行转换;有关详细信息,请参阅this earlier question

更新:噢,我应该知道如何使用我为图书馆提供的电话......

更新x2 :关于编译失败,我不认为这有资格作为错误。

我没有仔细查看这个特定代码的生成程序集,但是movsd指令的操作数被搞砸了。来自Intel x86 manual的§11.4.1.1:

  

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

在未优化的代码中,您有movsd LnTH(%rip),%xmm0之类的精细说明,但在-O代码中,您会看到movsd Ln2cJ(%rip),%rax之类的内容,其中%rax是通用的注册,而不是XMM注册。

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

由于处理这个需要优化器放弃许多假设,让它在正常情况下发出更好的代码,我倾向于说这不是一个错误,而是一个很好的故事,为什么unsafe功能警告警告。

答案 1 :(得分:1)

请注意,使用unsafeCoerce#在这里很危险,文档说

  

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

关于速度,避免中间列表并通过unsafeCreate的{​​{1}}直接写入内存可能会更快。

答案 2 :(得分:1)

根据acfoltzer(谷物源代码)和Daniel Fischer(unsafeCreate)的建议,我编写了下面适用于我的用例的代码,并且速度很快:

{-#LANGUAGE MagicHash #-}
import Data.ByteString (pack, unpack)
import Data.ByteString.Internal (unsafeCreate,ByteString)
import Data.Bits (shiftR)
import GHC.Int (Int64)
import GHC.Prim
import GHC.Types
import GHC.Word
import Unsafe.Coerce
import Criterion.Main
import Foreign

-- | Write a Word64 in little endian format
putWord64le :: Word64 -> Ptr Word8 -> IO()
putWord64le w p = do
  poke p               (fromIntegral (w)           :: Word8)
  poke (p `plusPtr` 1) (fromIntegral (shiftR w  8) :: Word8)
  poke (p `plusPtr` 2) (fromIntegral (shiftR w 16) :: Word8)
  poke (p `plusPtr` 3) (fromIntegral (shiftR w 24) :: Word8)
  poke (p `plusPtr` 4) (fromIntegral (shiftR w 32) :: Word8)
  poke (p `plusPtr` 5) (fromIntegral (shiftR w 40) :: Word8)
  poke (p `plusPtr` 6) (fromIntegral (shiftR w 48) :: Word8)
  poke (p `plusPtr` 7) (fromIntegral (shiftR w 56) :: Word8)

{-# INLINE putWord64le #-}

encodeDouble :: Double -> ByteString
encodeDouble x = unsafeCreate 8 (putWord64le $ unsafeCoerce x)

main :: IO ()
main = defaultMain [
        bgroup "encodeDouble" [
          bench "78901.234"  $ whnf encodeDouble 78901.234
          , bench "789.01" $ whnf encodeDouble 789.01
          ]
       ]

标准输出(截断):

estimating cost of a clock call...
mean is 46.80361 ns (35 iterations)
found 5 outliers among 35 samples (14.3%)
  3 (8.6%) high mild
  2 (5.7%) high severe

benchmarking encodeDouble/78901.234
mean: 18.80689 ns, lb 18.73805 ns, ub 18.97247 ns, ci 0.950
std dev: 516.7499 ps, lb 244.8588 ps, ub 1.043685 ns, ci 0.950

benchmarking encodeDouble/789.01
mean: 18.96963 ns, lb 18.90986 ns, ub 19.06127 ns, ci 0.950
std dev: 374.2191 ps, lb 275.3313 ps, ub 614.4281 ps, ci 0.950

从~220ns下降到~19ns,真好!我没有做任何花哨的编译。只有-O标志将在GHC7(Mac,x86_64)中完成。

现在,试着找出如何快速完成双打列表!