5

以下のデータ型の格納可能なベクトル インスタンスを作成しました (元の質問はこちら):

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 からのフィードバックに基づいて、アライメントを書き直し、コンストラクターを Word8 ではなく Word32 型に更新しました。しかし、これを有効にするには、データ コンストラクターもアンパックされた値を持つように更新する必要があるようです。これは私の見落としでした。そもそもアンパックされた値を持つようにデータ コンストラクターを作成する必要がありました ( John Tibbell によるパフォーマンス スライド- スライド #49 を参照)。そのため、データ コンストラクターを書き直し、アライメントとコンストラクターの変更を加えることで、パフォーマンスに大きな影響を与え、ベクターに対する関数 (私のベンチマーク テストでは単純な合計関数) で約 33% 向上しました。以下の関連する変更 (警告 - 移植性はありませんが、私のユースケースでは問題ではありません):

データ コンストラクターの変更:

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

保存可能な sizeof と配置の変更:

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
4

2 に答える 2

3

4バイトまたは8バイトで整列されたメモリアクセスは、通常、奇妙に整列されたアクセスよりもはるかに高速です。インスタンスのアラインメントは自動的に8バイトに切り上げられる可能性がありますが、コンストラクタータグに32ビット(Int32またはWord32)を使用し、両方のタイプのペイロードの読み取りと書き込みを行う、明示的な8バイトのアラインメントで少なくとも測定することをお勧めしますとしてInt32。それは少し無駄になりますが、より速くなる可能性は十分にあります。64ビットプラットフォームを使用しているため、16バイトのアライメントと読み取り/書き込みを使用するとさらに高速になる場合がありますInt64。ベンチマーク、ベンチマーク、ベンチマークは、何が最も役立つかを見つけます。

于 2011-12-09T19:59:38.247 に答える
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

これは、現在のプロセッサアーキテクチャでより高速なデータ構造を実現するために支払う必要のある価格です。

于 2011-12-09T20:37:06.333 に答える