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

Optimization 为union结构编写可存储向量定义时的优化建议,optimization,haskell,vector,storable,Optimization,Haskell,Vector,Storable,我为下面的数据类型()编写了一个可存储的向量实例: 下面是为可存储向量定义这些实例的代码。虽然我使用下面的代码获得了非常好的性能,但我对改进可存储实例性能的一般建议非常感兴趣。我所说的一般性建议是指以下内容: 它不是特定于GHC编译器版本的。您可以假设GHC 6.12.3+排除早期版本中存在的性能缺陷,并且与此处的代码相关 平台特定的建议是可以的。您可以假设x86_64 Linux平台 与利用硬件特定优化的建议相比,更具算法改进(big O)形式的通用建议更具价值。但是,考虑到像peek/po

我为下面的数据类型()编写了一个可存储的向量实例:

下面是为可存储向量定义这些实例的代码。虽然我使用下面的代码获得了非常好的性能,但我对改进可存储实例性能的一般建议非常感兴趣。我所说的一般性建议是指以下内容:

  • 它不是特定于GHC编译器版本的。您可以假设GHC 6.12.3+排除早期版本中存在的性能缺陷,并且与此处的代码相关
  • 平台特定的建议是可以的。您可以假设x86_64 Linux平台
  • 与利用硬件特定优化的建议相比,更具算法改进(big O)形式的通用建议更具价值。但是,考虑到像peek/poke这样的基本操作,据我所知,算法改进的空间不大(因此更具价值,因为它是一种稀缺商品:)
  • x86_64的编译器标志是可接受的(例如,告诉编译器删除浮点安全检查等)。我使用“-O2--make”选项编译代码
如果有任何已知的好的库源代码可以做类似的事情(例如,为联合/递归数据类型定义可存储实例),我将非常有兴趣检查它们

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
可存储大小和对齐更改:

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
实例可存储原子,其中
sizeOf=2*sizeOf(未定义::Int32)
对准度=4
{-#内联窥视}
peek p=do
设p1=(castPtr p::Ptr Word32)`plusPtr`1
不行
x do
x do
poke(castPtr p::Ptr Word32)0
pokeElemOff(castPtr p1)0 a
a->do
poke(castPtr p::Ptr Word32)1
pokeElemOff(castPtr p1)0 a
其中p1=(castPtr p::Ptr Word32)`plusPtr`1

四字节或八字节对齐的内存访问通常比奇怪对齐的访问快得多。您实例的对齐方式可能会自动四舍五入到八个字节,但我建议至少使用显式八字节对齐方式进行测量,使用32位(
Int32
Word32
)作为构造函数标记,并将这两种类型的有效负载读写为
Int32
。那会浪费一些,但很有可能会更快。由于您在64位平台上,使用16字节对齐和读/写
Int64
可能会更快。基准测试,基准测试,基准测试,找出什么最适合你。

如果你追求的是速度,那么这种比特打包不是正确的方向

处理器总是处理字大小的操作,这意味着如果您有例如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
|  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
如您所见,数据越来越与字边界不同步,这使得处理器/程序必须做更多的工作才能访问每个元素

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

|  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
|  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,因为您不会以任何方式将这些字节用于其他任何内容,并且使所有数据元素word对齐将进一步提高速度:

|  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

这是在当前处理器体系结构上为更快的数据结构所必须付出的代价。

我不认为代码是可移植的。未对齐的访问只适用于少数体系结构。另一方面,位打包允许您将更多数据打包到有限的缓存中,这一点也非常重要。权衡,和往常一样。是的,伊利修斯,我也考虑过缓存权衡。一级/二级缓存未命中相当昂贵。