13

前の質問へのコメントで、私は次のように主張しました。

ghc-7.4.1+llvm が厳密なデータ フィールドで列挙型のアンパックを行うことを示す別のベンチマークがあります。

実際、いくつかの実験の後、少なくともいくつかの単純化されたケースでは、列挙型を使用することは少なくとも同じくらい高速であり、実際には newtype の Word8 を使用するよりもメモリ効率が高い (したがって、より現実的なアプリケーションではより高速である) と私は信じています。 (または Int)、データ型の厳密なフィールドで使用された場合でも。前の質問で述べたように、私はより現実的な (しかしまだ小さい) 設定で同様の現象を経験しました。

ghc/llvm が列挙に対してどのような最適化を行うかについて、関連する参考文献を教えてもらえますか? 特に、厳密なデータ フィールドの列挙型の内部タグを実際に展開しますか? アセンブリの出力とプロファイリングの結果は、それが事実であることを示唆しているように見えますが、私にはコアレベルでの兆候はありません. どんな洞察も大歓迎です。

もう 1 つの質問: 列挙型は常に、対応する Integral の newtype と少なくとも同じくらい効率的ですか? (列挙も積分のように振る舞うことができることに注意してください。)そうでない場合、(うまくいけば現実的に役立つ)例外は何ですか?Daniel Fischer は、彼の回答で、複数コンストラクターのデータ型の厳密なフィールドに列挙を配置すると、特定の最適化が妨げられる可能性があると示唆しました。ただし、コンストラクターが 2 つの場合、これを検証できませんでした。それらを大きな複数コンストラクターのデータ型に入れると、違いがあるのではないでしょうか?

また、次のベンチマークで正確に何が起こっているのかにも興味があります。4 つのケースすべてで、ほぼ同じ量のバイトがヒープに割り当てられました。ただし、列挙の場合、GC が実際に行ったコピーは少なく、最大常駐数は newtype に比べて小さくなりました。

(実際、私の本当の質問は、パフォーマンスが重要な場合に、列挙型を newtypes に変換しようとする価値があるかどうかということでしたが、もう少し具体的にする方が役立つかもしれないと思いました。)

この質問の考えられる意味は次のとおりです。プログラムで多数の Int を使用していて、実際には非常に小さなサブセットで変化する場合、それらを (ボックス化されていない型ではなく) 列挙型に変更すると、パフォーマンスが低下する可能性があります。 gain (ただし、厳密には注意してください)。

以下は、ベンチマークの概要であり、その後にベンチマーク コードと、システムでテストするための便利な makefile が続きます。

ベンチマーク d
平均: 11.09113 ns、lb 11.06140 ns、ub 11.17545 ns、ci 0.950
標準偏差: 234.6722 ps、lb 72.31532 ps、ub 490.1156 ps、ci 0.950

ベンチマーク e
平均: 11.54242 ns、lb 11.51789 ns、ub 11.59720 ns、ci 0.950
標準偏差: 178.8556 ps、lb 73.05290 ps、ub 309.0252 ps、ci 0.950

ベンチマーク
平均: 11.74964 ns、lb 11.52543 ns、ub 12.50447 ns、ci 0.950
標準偏差: 1.803095 ns、lb 207.2720 ps、ub 4.029809 ns、ci 0.950

ベンチマーク t
平均: 11.89797 ns、lb 11.86266 ns、ub 11.99105 ns、ci 0.950
標準偏差: 269.5795 ps、lb 81.65093 ps、ub 533.8658 ps、ci 0.950

OK、列挙型は、少なくとも newtype よりも効率的であるように見えます
次に、関数のヒーププロファイル
heapTest x = print $ head $ force $ reverse $ take 100000 $ iterate (force . succ') x

データ D = A | ビ | 子:
      ヒープに割り当てられた 10,892,604 バイト
       GC 中にコピーされた 6,401,260 バイト
       1,396,092 バイトの最大常駐 (3 サンプル)
          55,940 バイトの最大スロップ
               合計 6 MB の使用メモリ (断片化により 0 MB が失われる)
  生産性 総ユーザーの 47.8%、総経過時間の 35.4%

newtype E = E Word8:
      ヒープに割り当てられた 11,692,768 バイト
       GC 中に 8,909,632 バイトがコピーされました
       2,779,776 バイトの最大常駐 (3 サンプル)
          92,464 バイトの最大スロップ
               合計 7 MB の使用メモリ (断片化により 0 MB が失われる)
  生産性 総ユーザーの 36.9%、総経過時間の 33.8%

データ S = S !D:
      ヒープに割り当てられた 10,892,736 バイト
       GC 中にコピーされた 6,401,260 バイト
       1,396,092 バイトの最大常駐 (3 サンプル)
          55,940 バイトの最大スロップ
               合計 6 MB の使用メモリ (断片化により 0 MB が失われる)
  生産性 総ユーザーの 48.7%、総経過時間の 33.3%

データ T = T {-# UNPACK #-} !E:
      ヒープに割り当てられた 11,692,968 バイト
       GC 中に 8,909,640 バイトがコピーされました
       2,779,760 バイトの最大常駐 (3 サンプル)
          92,536 バイトの最大スロップ
               合計 7 MB の使用メモリ (断片化により 0 MB が失われる)
  生産性 総ユーザーの 36.1%、総経過時間の 31.6%

コンストラクターが 2 つの場合でも、同様のパフォーマンスの向上が得られます。

ベンチマーク コード (EnumTest.hs として保存):


{-# LANGUAGE CPP,MagicHash , BangPatterns ,GeneralizedNewtypeDeriving #-}
module Main(main,d,e,s,t,D(..),E(..),S(..),T(..))
where       
import GHC.Base  
import Data.List
import Data.Word
import Control.DeepSeq
import Criterion.Main

data    D = A | B | C  deriving(Eq,Ord,Show,Enum,Bounded)
newtype E = E Word8    deriving(Eq,Ord,Show,Enum)

data    S = S                !D deriving (Eq,Ord,Show) 
data    T = T {-# UNPACK #-} !E deriving (Eq,Ord,Show)

-- I assume the following definitions are all correct --- otherwise
-- the whole benchmark may be useless
instance NFData D where
  rnf !x         = ()
instance NFData E where
  rnf (E !x)     = ()
instance NFData S where
  rnf (S !x)     = ()
instance NFData T where
  rnf (T (E !x)) = ()  

instance Enum S where
  toEnum         = S . toEnum
  fromEnum (S x) = fromEnum x 
instance Enum T where
  toEnum         = T . toEnum
  fromEnum (T x) = fromEnum x 

instance Bounded E where
  minBound = E 0
  maxBound = E 2
instance Bounded S where
  minBound = S minBound
  maxBound = S maxBound
instance Bounded T where
  minBound = T minBound
  maxBound = T maxBound

succ' :: (Eq a,Enum a,Bounded a) => a -> a
succ' x | x == maxBound = minBound
          | otherwise        = succ x

-- Those numbers below are for easy browsing of the assembly code
d :: D -> Int#
d x = case x of
  A -> 1234#
  B -> 5678#
  C -> 9412#

e :: E -> Int#
e x = case x of     
  E 0 -> 1357#
  E 1 -> 2468#
  E _ -> 9914#

s :: S -> Int#
s x = case x of     
  S A -> 9876#
  S B -> 5432#
  S C -> 1097#

t :: T -> Int#
t x = case x of     
  T (E 0) -> 9630#
  T (E 1) -> 8529#
  T (E _) -> 7418#


benchmark :: IO ()
benchmark = defaultMain [ bench "d" $ whnf d' A
                        , bench "e" $ whnf e' (E 0)
                        , bench "s" $ whnf s' (S A) 
                        , bench "t" $ whnf t' (T (E 0))
                        ]
  where
    d' x = I# (d x)
    e' x = I# (e x)
    s' x = I# (s x)
    t' x = I# (t x)    

heapTest :: (NFData a,Show a,Eq a,Enum a,Bounded a) => a -> IO ()
heapTest x = print $ head $ force $ reverse $ take 100000 $ iterate (force . succ') x

main :: IO ()
main =    
#if   defined TEST_D
     heapTest (A :: D)
#elif defined TEST_E                
     heapTest (E 0 :: E)
#elif defined TEST_S
     heapTest (S A :: S)
#elif defined TEST_T                
     heapTest (T (E 0) :: T)
#else
     benchmark
#endif

-- A minor rant: 
-- For reliable statistics, I hope Criterion will run the code in *random order*,
-- at least for comparing functions with the same type. Elapsed times on my system are just too
-- noisy to conclude anything.

ベンチマークに使用されるメイクファイル:


GHC=/usr/bin/ghc
# If you dont't like the ATT syntax in the output assembly, use this: -fllvm -optlc --x86-asm-syntax=intel
GHC_DEBUG_FLAGS= -keep-s-file -keep-llvm-file  # -optlc --x86-asm-syntax=intel
GHCFLAGS=-O2 -funbox-strict-fields -rtsopts -fllvm -fwarn-missing-signatures
GHC_MAKE=$(GHC) --make $(GHCFLAGS)
GHC_PROF_MAKE=$(GHC)  -prof  -auto-all -caf-all --make $(GHCFLAGS)

all : benchmark enumtest_all

enumtest_d : EnumTest.hs
    $(GHC_MAKE) -o $@ $^ -DTEST_D

enumtest_e : EnumTest.hs
    $(GHC_MAKE) -o $@ $^ -DTEST_E

enumtest_s : EnumTest.hs
    $(GHC_MAKE) -o $@ $^ -DTEST_S

enumtest_t : EnumTest.hs
    $(GHC_MAKE) -o $@ $^ -DTEST_T

enumtest_all : enumtest_d enumtest_e enumtest_s enumtest_t
    for x in $^; do ./$$x +RTS -sstderr ;done

benchmark : EnumTest
    time ./$^

% : %.hs
    $(GHC_MAKE) -o $@ $^

%.core : %.hs
    $(GHC)  -S  $(GHCFLAGS)   $(GHC_DEBUG_FLAGS) -ddump-simpl -dsuppress-all -dsuppress-coercions -ddump-stranal $^ > $@

clean :
    rm *.hi *.o *.core *.s enumtest_? ; true

どうもありがとうございました!

4

2 に答える 2

8

初め

Daniel Fischer は、彼の回答で、複数コンストラクターのデータ型の厳密なフィールドに列挙を配置すると、特定の最適化が妨げられる可能性があると示唆しました。

あなたはそれを誤解しました。型のコンストラクターがある場合C、その型に複数のコンストラクターがあるか 1 つだけであるかに関係なく、 type , の厳密なフィールドを持つT場合、が unpackable 型の newtype ラッパーである場合、厳密なフィールドは展開できますが、そうではありませんC ... !T ...TifTは列挙型です。原則として、型の値のコンストラクタ タグもアンパックできるはずですTが、GHC はそれを行いません (おそらく理由があり、私が考えるよりも複雑かもしれません)。それにもかかわらず、十分に小さい列挙型の場合、Mikhail Gushenkov によって言及されたポインターのタグ付けは、多かれ少なかれ同じ効果を持つはずです (完全ではないかもしれません)。

ただし、3 つまたは 7 つ (64 ビット ワードの場合) を超えるコンストラクターを持つ列挙型の場合、場合によってはポインターをたどる必要があるため、違いが明らかになるはずです。

第二に、への短い答え

実際、私の本当の質問は、パフォーマンスが重要な場合に列挙型を新しい型に変換しようとする価値があるかということでした。

時々。

変換によって実際にパフォーマンスが向上するかどうかまたどの程度向上するかは、値をどのように処理するかによって異なります。また、プログラムが遅くなる可能性もあります。また、より多くのメモリを使用する場合があります (以下を参照)。

一般的なルールはなく、それぞれのケースを評価する必要があります。newtype でラップされたInts の方が速いパターンもあれば、遅いパターンもあります。典型的なプログラムには両方のインスタンスが含まれており、どちらが優勢であるかを見つける必要があります。


ではベンチマークへ。

C代わりにAとのE 2代わりにを使用して、ベンチマークの引数を自由に変更しましたE 0。その結果、次の場合、newtype の方がわずかに高速でした。

warming up
estimating clock resolution...
mean is 1.549612 us (640001 iterations)
found 4506 outliers among 639999 samples (0.7%)
  3639 (0.6%) high severe
estimating cost of a clock call...
mean is 39.24624 ns (12 iterations)
found 2 outliers among 12 samples (16.7%)
  1 (8.3%) low mild
  1 (8.3%) high severe

benchmarking d
mean: 12.12989 ns, lb 12.01136 ns, ub 12.32002 ns, ci 0.950
std dev: 755.9999 ps, lb 529.5348 ps, ub 1.034185 ns, ci 0.950
found 17 outliers among 100 samples (17.0%)
  17 (17.0%) high severe
variance introduced by outliers: 59.503%
variance is severely inflated by outliers

benchmarking e
mean: 10.82692 ns, lb 10.73286 ns, ub 10.98045 ns, ci 0.950
std dev: 604.1786 ps, lb 416.5018 ps, ub 871.0923 ps, ci 0.950
found 10 outliers among 100 samples (10.0%)
  4 (4.0%) high mild
  6 (6.0%) high severe
variance introduced by outliers: 53.482%
variance is severely inflated by outliers

benchmarking s
mean: 13.18192 ns, lb 13.11898 ns, ub 13.25911 ns, ci 0.950
std dev: 354.1332 ps, lb 300.2860 ps, ub 406.2424 ps, ci 0.950
found 13 outliers among 100 samples (13.0%)
  13 (13.0%) high mild
variance introduced by outliers: 20.952%
variance is moderately inflated by outliers

benchmarking t
mean: 11.16461 ns, lb 11.02716 ns, ub 11.37018 ns, ci 0.950
std dev: 853.2152 ps, lb 602.5197 ps, ub 1.086899 ns, ci 0.950
found 14 outliers among 100 samples (14.0%)
  3 (3.0%) high mild
  11 (11.0%) high severe
variance introduced by outliers: 68.689%
variance is severely inflated by outliers

したがって、ベンチマークはどちらの場合も実質的な違いを示しておらず、全体的な結果は提供された引数に依存します。それぞれとBE 1、差は小さかったが、私の実行では、そこでもニュータイプが勝った。

ただし、clock呼び出しの推定コストは、これらの平均の約 4 倍であり、推定クロック分解能は 100 倍以上であることに注意してください。これらの結果が信頼できるものであるとは確信していません。私のシステムで観察された短いベンチマークの差異を考慮して、実行時間が 10 マイクロ秒未満のベンチマークは信用しません。結果がより安定し、外れ値が少なくなるため、テストをより長く実行することを好みます。

それにかんする

heapTest x = print $ head $ force $ reverse $ take 100000 $ iterate (force . succ') x

残念ながら、iterate (force . succ')リストの構成時にリストの要素を強制しないため、サンクのリストを取得し (深さを増す)、その最初のセグメントを逆にして、リスト要素を強制します。

したがって、そこで行われる作業と割り当ての圧倒的な部分は、サンクとリストの構築、そしてサンクの評価です。生成時に要素を強制することで大きなサンクの構築を防ぐと、より意味のある結果が得られます。

iterate' :: (a -> a) -> a -> [a]
iterate' f !a = a : iterate' f (f a)

(バング パターン - WHNF - は、問題の型の値を完全に評価するのに十分です)。

それでも、列挙型と newtype バリアントの間の割り当て数値には、観察可能で一貫した違いがあり、それは にも残りiterate'ます。また、最初のセグメントを反転して取得する代わりに、head単純に を取得するlist !! indexと、他の数値 (コピーされたバイト数、最大常駐数) が小さいため、さらに印象的になります。

その理由は、リスト要素はボックス化解除できないため、リスト セルには要素値へのポインターが含まれ、列挙値は共有されているため (プログラム全体で 1 つしか存在しませんA)、これらのポインターはすべて同じオブジェクトを指しています。ただし、整数は共有されないため、各リスト セルは異なるオブジェクトを指します。

割り当ての違いは、あなたのシステムではほぼ正確に 800,000 バイトですが、私のシステムでは 1,600,000 バイトです。

Word8それが 200,000 ワードに必要なものであり、100,000 (またはWord; 、...)の割り当てに必要なものIntです (値ごとに、コンストラクターに 1 ワード、Word#またはに 1 ワードInt#)。

于 2012-10-13T17:10:21.927 に答える
7

コンパイラの出力を見ずに、コードのニュータイプバージョンで速度が向上しないのは、ポインタのタグ付けが原因である可能性があると思います。x86では、GHCは、指定されたクロージャに関する情報のために、各ポインタに2ビットを予約します。00は「未評価または不明」を意味し、他の3つのケースは、評価されたコンストラクターの実際のタグをエンコードします。この情報は、ガベージコレクターによって動的に更新されます。テストデータ型には3つのケースしかないため、それらは常にタグビットに収まります。したがって、パターンマッチングで間接参照が必要になることはありません。データ型にケースを追加してみて、何が起こるかを確認してください。このペーパーで、動的ポインターのタグ付けに関する詳細情報を見つけることができます。

動的ポインタタグ付けを使用したより高速な怠惰

サイモン・マーロウ、アレクセイ・ロドリゲス・ヤクシェフ、サイモン・ペイトン・ジョーンズ、ICFP2007。

于 2012-10-13T14:41:49.100 に答える