2011-12-02 52 views
6

我寫了一個將64位Double轉換爲ByteString的函數(架構/類型安全性不是真的問題 - 現在讓我們假設Double是64位的Word)。雖然下面的函數運行良好,但我想知道是否有更快的方法將Double轉換爲ByteString。在下面的代碼中,將Word64解壓到Word8列表中,然後反向(使其成爲小端格式),然後打包到ByteString中。代碼如下:有效地將64位Double轉換爲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必須解壓到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包),直到這個錯誤得到修復,或者直到找到解決方法。看起來像有關GHC Ticket 4092。如果這是一個新的錯誤或其他錯誤,請糾正我。與標準基準 更新使用unsafeCoerce代碼修復編譯問題代碼如下:現在,我不能編譯:(

EDIT2。

{-# 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花費〜195ns。拆開word64與作爲word8的列表在這裏相當昂貴。

+0

我很好奇爲什麼你不想使用'穀物'的方法,它將核心中的少數幾行縮小爲鏈接的答案。一旦你開始處理列表,你將得到更昂貴的東西。 – acfoltzer

+0

acfoltzer,好點。我終於想出了我應該尋找的東西(putWord64le實現)。那就是訣竅。請參閱下面的帖子。如果您對在哪裏尋找快速列表實施有任何建議,請告訴我。 – Sal

回答

1

注意的是,使用unsafeCoerce#是這裏危險,文檔說

投拆箱到另一種拆箱類型相同的大小(但不是浮點和整數類型之間的強制

關於速度,避免中間列表可能會更快,並且可能會更快地通過unsafeCreateData.ByteString.Internal寫入內存。

+0

是的,我認爲這正是爲什麼編譯錯誤並不表示錯誤。 – acfoltzer

4

我最近增加了對IEEE-754浮點數的支持到cereal,您可以在data-binary-ieee754找到類似binary的函數。下面是一個使用cereal版本往返piByteString和背部的例子:

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

它採用與ST陣列的一招,迅速完成轉換;請參閱this earlier question瞭解更多詳情。

更新:D'哦,我應該知道如何使用電話我到圖書館貢獻......

更新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函數有一個警告空格警告。

+0

謝謝。這很有用,因爲我無法用unsafecoerce編譯我的代碼(請參閱上面的編輯以獲取更新) – Sal

+0

請參閱我的更新,以瞭解爲什麼在可預見的將來,您可能無法使用'unsafeCoerce'編譯:) – acfoltzer

+0

當然,正如鏈接票據暗示的那樣,未來可能會對GHC內置專門的強制措施,但是'unsafeCoerce'可能不會以這種方式工作 – acfoltzer

1

繼acfoltzer的建議(穀物源代碼),和丹尼爾·菲捨爾(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)中執行。

現在,試圖找出如何快速做到雙打列表!