Haskell 高效地将64位Double转换为ByteString
我编写了一个函数来将64位Double转换为ByteString(体系结构/类型安全性不是真正的问题-现在让我们假设Double是64位字)。虽然下面的函数运行良好,但我想知道是否有更快的方法将Double转换为ByteString。在下面的代码中,有一个将Word64解包到Word8列表中,然后是reverse(使其成为小endian格式),然后打包到ByteString中。代码如下:Haskell 高效地将64位Double转换为ByteString,haskell,casting,bytestring,Haskell,Casting,Bytestring,我编写了一个函数来将64位Double转换为ByteString(体系结构/类型安全性不是真正的问题-现在让我们假设Double是64位字)。虽然下面的函数运行良好,但我想知道是否有更快的方法将Double转换为ByteString。在下面的代码中,有一个将Word64解包到Word8列表中,然后是reverse(使其成为小endian格式),然后打包到ByteString中。代码如下: {-# LANGUAGE MagicHash #-} import GHC.Prim import GHC.
{-# 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发送之前,使用它将大量的双值编码到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(grane/data-binary-ieee754软件包),直到这个错误被修复,或者直到我找到解决方法。看起来像是和我有关。请纠正我,如果这是一个新的错误,或一个不同的错误。目前,我无法编译它:(
EDIT2:
更新代码以使用Unsafeccerce修复了编译问题。下面的代码使用标准基准:
{-# 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
]
]
标准输出(截断):
进一步分析,大部分瓶颈似乎都在unpack64中。强制需要约6ns。unpack64需要约195ns。将word64作为word8列表解包在这里是相当昂贵的。请注意,使用
unsecfeceorce#
在这里是危险的,文档说
将一个未绑定类型强制转换为另一个大小相同的未绑定类型(,但不强制浮点类型和整数类型之间的转换))
关于速度,避免使用中间列表,直接从
数据通过unfectreate
写入内存可能会更快。ByteString.Internal
我最近添加了对IEEE-754 float的支持,您可以在中找到与binary
类似的函数通过TestRing返回到a的往返pi
,然后返回:
Prelude Data.Serialize> runGet getFloat64be $ runPut $ putFloat64be pi
Right 3.141592653589793
它使用ST数组的技巧快速进行转换;有关更多详细信息,请参阅
更新:哦,我应该知道如何使用我给图书馆的电话
更新x2:关于编译失败,我不认为这是一个bug
我没有仔细查看针对该特定代码生成的程序集,但是movsd
指令的操作数被弄脏了。根据该指令的§11.4.1.1:
MOVSD(移动标量双精度浮点)将64位双精度浮点操作数从内存传输到XMM寄存器的低位四字,反之亦然,或在XMM寄存器之间传输
在未优化的代码中,您有一些很好的指令,如movsd LnTH(%rip),%xmm0
,但在-O
代码中,您会看到类似movsd Ln2cJ(%rip),%rax
,其中%rax
是一个通用寄存器,而不是XMM寄存器
优化器可能会根据所涉及的数据类型对需要在寄存器之间移动的数据表示形式做出假设。unsafeccerce
和friends会使这些假设失效,因此当指令选择器认为它为D#
选择了正确的操作时,它实际上会发出尝试在W64
适合的地方填充D
由于处理这一问题需要优化器放弃许多假设,让它在正常情况下发出更好的代码,因此我倾向于说这不是一个bug,而是一个很好的故事,解释了为什么不安全的函数带有一个警告或警告。遵循acfoltzer(谷物源代码)的建议,和Daniel Fischer(未完成创建),我编写了下面的代码,该代码非常适合我的用例,而且速度也很快:
{-#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
]
]
标准输出(截断):
从~220ns到~19ns,很好!我在编译时没有做任何花哨的事情。GHC7(Mac,x86_64)中只需-O标志即可
现在,试着找出如何快速使用双打列表!谢谢。这很有用,因为我不能用unsecfect编译我的代码(请参阅上面的编辑以获取更新)请参阅我的更新,了解在可预见的未来你可能无法用unsecfect编译的原因:)当然,正如链接的票证所提到的,将来可能会在GHC中内置专门的强制,但是unsafeccerce
可能永远不会以这种方式工作编译错误指针非常有用。我同意买主的警告。我也在想,在执行Unsafeccerce时,是否确实存在违反数据宽度假设的情况。是的,我认为这正是编译错误不表明存在错误的原因。我很好奇,为什么您不想使用grane
中的方法,这种方法将核心中的几行作为链接的答案注释。一旦你开始处理清单,你就会得到更昂贵的东西。阿佛兹,说得对。我终于找到了我应该寻找的东西(putWord64le实现)。这就成功了。请看下面我的帖子。如果您对在哪里寻找快速列表实现有任何建议,请让我知道。
{-#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