I wrote a function to convert 64-bit Double to ByteString (architecture/type safety is not really an issue - let us assume for now that the Double is 64-bit Word). While the function below works well, I am wondering if there is a faster way to convert the Double to ByteString. In the code below, there is one unpack of Word64 into Word8 list, followed by reverse (to make it little endian format), and then packing into ByteString. The code is below:
{-# 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
A sample GHCi output on Mac x86:
*Main> bprint $ encodeDouble 7234.4
"0x666666666642bc40"
While the code seems to work well, I plan to use it for encoding lot of Double values into ByteString before sending it over IPC. So, I will appreciate pointers on making it faster, if there are any.
It does seem to me that double must be unpacked into Word8, and then packed into ByteString. So, may be the overall algorithm as it is, can't be improved upon much. But, using a more efficient unpack/pack function will probably make a difference, if there were one.
EDIT1: I just discovered another complication on Mac (GHC 7.0.3) - the code above won't compile in GHC because of this error - I was testing in GHCi so far:
$ 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'
So, it looks like I have to fall back on FFI (cereal/data-binary-ieee754 package) until this bug is fixed, or until I find a workaround. Looks like related to GHC Ticket 4092. Please correct me if this is a new bug, or a different bug. For now, I can't compile it :(
EDIT2: Updating the code to use unsafeCoerce fixes the compilation issue. Code below with Criterion benchmark:
{-# 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
]
]
Criterion Output (truncated):
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
On further analysis, most of the bottleneck seems to be in unpack64. Coercion takes ~6ns. unpack64 takes ~195ns. Unpacking the word64 as a list of word8 is quite expensive here.
cereal
, which reduces to just a handful of lines in the Core as the linked answer notes. As soon as you start dealing with lists, you're going to wind up with something much more expensive. – Honeybunch