为union结构编写可存储的向量定义时的优化建议

时间:2011-12-09 19:44:11

标签: optimization haskell vector storable

我为下面的数据类型(original question here)写了一个可存储的矢量实例:

data Atoms = I GHC.Int.Int32 | S GHC.Int.Int16

下面是为Storable vector定义这些实例的代码。虽然我使用下面的代码获得了非常好的性能,但我对通用建议非常感兴趣,以提高该可存储实例的性能。通用建议,我的意思是:

  • 它不是特定于GHC编译器版本。您可以假设GHC 6.12.3+排除了早期版本中存在的性能错误,并且与此处的代码相关。
  • 特定于平台的建议是可以的。您可以假设x86_64 Linux平台。
  • 与利用特定于硬件的优化的建议相比,更多以算法改进(大O)形式的通用建议非常受重视。但是,考虑到像偷看/捅这样的基本操作,据我所知,算法改进的空间不大(因此它更有价值,因为它是一种稀缺的商品:)
  • x86_64的编译器标志是可接受的(例如,告诉编译器有关删除浮点安全检查等)。我正在使用“-O2 --make”选项来编译代码。

如果有任何已知的好的库源代码做类似的事情(即,为union / recursive数据类型定义可存储的实例),我将非常有兴趣检查它们。

import Data.Vector.Storable
import qualified Data.Vector.Storable as V
import Foreign
import Foreign.C.Types
import GHC.Int

data Atoms = I GHC.Int.Int32 | S GHC.Int.Int16
                deriving (Show)

instance Storable Atoms where
  sizeOf _ = 1 + sizeOf (undefined :: Int32)
  alignment _ = 1 + alignment (undefined :: Int32)

  {-# INLINE peek #-}
  peek p = do
            let p1 = (castPtr p::Ptr Word8) `plusPtr` 1 -- get pointer to start of the    element. First byte is type of element
            t <- peek (castPtr p::Ptr Word8)
            case t of
              0 -> do
                    x <- peekElemOff (castPtr p1 :: Ptr GHC.Int.Int32) 0
                    return (I x)
              1 -> do
                    x <- peekElemOff (castPtr p1 :: Ptr GHC.Int.Int16) 0
                    return (S x)

  {-# INLINE poke #-}
  poke p x = case x of
      I a -> do
              poke (castPtr p :: Ptr Word8) 0
              pokeElemOff (castPtr p1) 0 a
      S a -> do
              poke (castPtr p :: Ptr Word8) 1
              pokeElemOff (castPtr p1) 0 a
      where  p1 = (castPtr p :: Ptr Word8) `plusPtr` 1 -- get pointer to start of the     element. First byte is type of element

更新

根据Daniel和dflemstr的反馈,我重写了对齐,并且还将构造函数更新为Word32而不是Word8。但是,为了使其有效,数据构造函数似乎也应更新为具有解压缩值 - 这对我来说是一种疏忽。我应该编写数据构造函数以获得解压缩的值(参见performance slides by John Tibbell - 幻灯片#49)。因此,重写数据构造函数,加上对齐和构造函数更改,对性能产生了很大影响,对于向量上的函数(在我的基准测试中是一个简单的求和函数),它的性能提高了大约33%。下面的相关更改(警告 - 不可移植,但对我的用例来说不是问题):

数据构造函数更改:

data Atoms = I {-# UNPACK #-} !GHC.Int.Int32 | S {-# UNPACK #-} !GHC.Int.Int16

可存储的尺寸和对齐变化:

instance Storable Atoms where
  sizeOf _ = 2*sizeOf (undefined :: Int32)
  alignment _ = 4

  {-# INLINE peek #-}
  peek p = do
            let p1 = (castPtr p::Ptr Word32) `plusPtr` 1
            t <- peek (castPtr p::Ptr Word32)
            case t of
              0 -> do
                    x <- peekElemOff (castPtr p1 :: Ptr GHC.Int.Int32) 0
                    return (I x)
              _ -> do
                    x <- peekElemOff (castPtr p1 :: Ptr GHC.Int.Int16) 0
                    return (S x)

  {-# INLINE poke #-}
  poke p x = case x of
      I a -> do
              poke (castPtr p :: Ptr Word32) 0
              pokeElemOff (castPtr p1) 0 a
      S a -> do
              poke (castPtr p :: Ptr Word32) 1
              pokeElemOff (castPtr p1) 0 a
      where  p1 = (castPtr p :: Ptr Word32) `plusPtr` 1

2 个答案:

答案 0 :(得分:3)

四或八字节对齐的存储器访问通常比奇数对齐的访问快得多。您的实例的对齐可能会自动舍入到八个字节,但我建议至少使用32位(Int32Word32)进行显式八字节对齐测量构造函数标记以及将这两种类型的有效负载读取和写入Int32。这会浪费一些,但很有可能会更快。由于您使用的是64位平台,因此使用16字节对齐和读/写Int64可能更快。基准,基准,基准,找出最适合你的东西。

答案 1 :(得分:1)

如果速度是你所追求的,那么这种有点包装并不是正确的方向。

处理器始终处理字大小的操作,这意味着如果您有例如一个32位处理器,处理器可以(物理上)处理的最小内存量是32位或4字节(对于64位处理器,它的64位或8字节)。进一步;处理器只能在字边界处加载存储器,这意味着字节地址是字大小的倍数。

因此,如果你使用5的对齐(在这种情况下),这意味着你的数据存储如下:

|  32 bits  |  32 bits  |  32 bits  |  32 bits  |
 [    data    ] [    data    ] [    data    ]
 00 00 00 00 01 01 00 01 00 00 00 12 34 56 78 00
 IX Value       IX Value XX XX IX Value

IX = Constructor index
Value = The stored value
XX = Unused byte

正如您所看到的,数据越来越与字边界不同步,使得处理器/程序必须做更多工作才能访问每个元素。

如果将对齐增加到8(64位),则数据将按如下方式存储:

|  32 bits  |  32 bits  |  32 bits  |  32 bits  |  32 bits  |  32 bits  |
 [    data    ]          [    data    ]          [    data    ]
 00 00 00 00 01 00 00 00 01 00 01 00 00 00 00 00 00 12 34 56 78 00 00 00
 IX Value       XX XX XX IX Value XX XX XX XX XX IX Value       XX XX XX

这会让你“浪费”每个元素3个字节,但是你的数据结构要快得多,因为每个数据都可以用更少的指令和对齐的内存负载加载和解释。

如果你打算继续使用8个字节,你也可以将你的构造函数索引转换为Int32,因为你还没有将这些字节用于其他任何东西,并使你的所有字对齐的基准元素进一步提高了速度:

|  32 bits  |  32 bits  |  32 bits  |  32 bits  |  32 bits  |  32 bits  |
 [        data         ] [        data         ] [        data         ]
 00 00 00 00 00 00 00 01 00 00 00 01 00 01 00 00 00 00 00 00 12 34 56 78
 Index       Value       Index       Value XX XX Index       Value

这是您在当前处理器架构上为更快的数据结构付出的代价。

相关问题