Intel TSX HLE を触ってみた	
x86/x64最適化勉強会6
2013-08-31
星野喬@サイボウズ・ラボ / @starpoz	

1
自己紹介	
•  星野 喬(@starpoz)
–  サイボウズ・ラボ

•  興味
–  データベース,ストレージ,分散アルゴリズムなど

•  今の仕事: #walbdev
–  Linux における block device の効率的な
増分記録ドライバ
•  http://developer.cybozu.co.jp/tech/?p=5130‎
2
今日の話	
•  Intel の最新 CPU (Haswell) の
新機能のひとつ TSX HLE を使い
性能評価をしてみて
トランザクショナルメモリの可能性を探る
•  後追い実験大歓迎 J

3
トランザクショナルメモリ (TM)	
•  「Intel TSX について」参照
–  http://www.slideshare.net/starpos/intel-tsxx86opti4

•  要約: atomic メモリアクセスを実現する技術
–  他と競合したくない一連のメモリアクセス
–  Lock-free な時代がすぐそこに(反論アリ

4
Intel TSX	
•  Transactional Synchronization eXtension
•  2 つのインターフェースを提供
–  HLE: Lock prefix の拡張で細粒度の排他を実現
–  RTM: 制約はあるが HardwareTM そのもの

•  楽観的な振舞
•  CPU L1 キャッシュ上で必要なデータを管理
•  キャッシュライン単位での競合検出

5
競合 (collision)	
•  あるリソースへの複数主体からのアクセスが
同時に起きること(要出典)
•  Intel TSX の場合:
–  リソース: キャッシュライン単位のメモリ領域
–  主体: スレッド
–  アクセス: read/write (全て read の場合を除く)
–  同時: クリティカルセクションの実行期間が重複	

6
HLE: Hardware Lock Elision	
競合発生しないケース	

競合発生するケース	

時間	

ロックを無視して
投機実行開始	

競合発生しなかった
めでたしめでたし	

時間	

競合発生
ロック取れたので
変更破棄
必ず実行できる	
ロック待ち体制	

•  楽観的挙動 –(失敗)à 悲観的挙動
•  失敗すると電気代が無駄になる
7
Spinlock with HLE	
•  GCC 4.8 の場合 (マニュアル参照)
class SpinlockHle
{
private:
char &lock_;
public:
explicit SpinlockHle(char &lock) : lock_(lock) {
int flags = __ATOMIC_ACQUIRE | __ATOMIC_HLE_ACQUIRE;
while (__atomic_exchange_n(&lock_, 1, flags))
_mm_pause();
}
~SpinlockHle() noexcept {
int flags = __ATOMIC_RELEASE | __ATOMIC_HLE_RELEASE;
__atomic_clear(&lock_, flags);
}
};	
8
HLE on/off の違い	
--- t1.hle0.s
2013-08-31 09:23:58.000000000 +0900
+++ t1.hle1.s
2013-08-31 09:23:30.000000000 +0900
@@ -13,12 +13,12 @@
.L3:
rep nop
.L2:
movl
%edx, %eax
xchgb
-1(%rsp), %al
+
xacquire xchgb -1(%rsp), %al
testb
%al, %al
jne
.L3
movb
$0, -1(%rsp)
+
xrelease movb
$0, -1(%rsp)
xorl
%eax, %eax
ret
.cfi_endproc
.LFE859:	
9
実験環境	
•  CPU: Core i7-4770
–  4cores 8HT
–  HT 有効
–  TurboBoost 有効

•  メモリ: 16GB
•  OS: Ubuntu 13.04 x86_64 kernel 3.8.19
•  コンパイラ: GCC 4.8.1
–  最適化フラグ: -O2 のみ

10
実験方法	
•  スレッドを必要なだけ起動して,X 秒間クリティカル
セクション (CS) を繰り返し実行
•  Spinlock を使って CS の排他を取る
–  HLE 有効/無効は実験パラメータ
–  終了判定のために CS 実行毎に atomic<bool> を load
するオーバーヘッドあり

•  CS の実行回数の合計値を X で割ったものを
スループットとする
•  上記実験を Y 回行い,スループットの avg, min,
max を計算
11
Experiments	
• 
• 
• 
• 
• 

Expr1:
Expr2:
Expr3:
Expr4:
Expr5:

simple counter(s)
counter(s) with delay
collision timing
access area size
map operations	

12
Expr1: simple counter(s)	
•  クリティカルセクションループ
while (!isEnd_.load(std::memory_order_relaxed)) {
SpinlockHle<useHLE> lk(mutex_);
counter_++;
}	

•  パラメータ
–  競合率 100%: counter を全スレッドで共有
–  競合率 0%: スレッド毎に counter を持つ
•  キャッシュラインが異なるように 64byte 毎に配置
Counter	
Thread	
競合率 100%	

Thread	
競合率 0%	

Counter	
13
Expr1: result	

44.7x	

10 sec, 20 trials	

spinlock なしの 8 threads 実行で 4309M なので,
isEnd.load() のオーバーヘッドは 8% 程度	

14
Expr1: result (scaled)	

競合100% のときは
オーバーヘッドの分
性能悪化	

10 sec, 20 trials	

15
Expr2: counter(s) with delay 	
•  1us delay (誤差は実測 70ns 以下)
void delayUsec(uint64_t usec) {
auto ts0 = std::chrono::high_resolution_clock::now();
uint64_t diff = 0;
while (diff < usec * 1000) {
auto ts1 = std::chrono::high_resolution_clock::now();
diff = std::chrono::duration_cast<
std::chrono::nanoseconds>(ts1 - ts0).count();
}
}	

•  クリティカルセクション
while (!isEnd_.load(std::memory_order_relaxed)) {
SpinlockHle<useHLE> lk(mutex_);
counter_++;
delayUsec(1);
}	

16
Expr2: result	

10 sec, 20 trials	

cs が 1us でも最大 5 倍程度の性能アップを期待できる	

17
Expr3: 	
•  1us delay の前/後にカウンタを更新する
while (!isEnd_.load(std::memory_order_relaxed)) {
SpinlockHle<useHLE> lk(mutex_);
if (isBefore) delayUsec(1);
counter_++;
if (!isBefore) delayUsec(1);
}	

•  目的
–  競合発覚するのが CS の最初か最後かで楽観的
実行が失敗する頻度が変化するのを観察したい

18
Expr3: result	

10 sec, 20 trials	

19
Expr4: 	
•  目的
–  write buffer や read flag を管理する領域は有
限なので,HLE で恩恵が受けられるアクセスサイ
ズの上限を知りたい

•  手段
–  X 個の 64bytes メモリ断片をスレッド毎に用意
–  CS の中で Y 個にアクセスする(重複アリ)
–  他の条件を同じにするため,Y は固定
–  2 <= X <= Y で評価
–  今回は write の評価のみ	
20
Expr4: 1-4 thread 16 clines	

1 threads	

3 threads	
10 sec, 20 trials	

2 threads	

4 threads	

CS 内では 12 lines までのアクセスにした方が良さそう	

21
Expr4: 5-8 thread 16 clines	

5 threads	

7 threads	
10 sec, 20 trials	

6 threads	

8 threads	

安定しない結果.12 lines を越えても効果があるケースも	

22
Expr4: 5-8 thread 64 clines	

5 threads	

7 threads	
5 sec, 100 trials	

6 threads	

8 threads	

安定しない結果,実験方法に問題があるかも?	

23
Expr5: 	
•  目的:
–  実用的なデータ構造で HLE の効果を知る

•  手段:
–  std::map<uint32_t, uint32_t> をひとつの
spinlock で排他
–  read 比率を変える: 0%, 90%, 99%, 100%
•  read 操作: ランダムキーで lower_bound 検索
•  write 操作: ひとつ削除,その後 insert

–  初期アイテム数: 10K (約2MB), 1M (約100MB)
–  (ついでに自作の btree map でも試す)
24
20131022追記	
•  以下 expr5 の結果グラフのスループットは全
て誤って 3 倍に集計されていたことが発覚
–  集計スクリプトのミス

•  スループットを見るときは表記の 1/3 にして
ご覧ください	

25
Expr5: 10K items, read 0%	

std::map はスレッド数増加で HLE on が逆転
性能上昇は最大で 39% (8 threads)	

btree も同様の傾向	

26
Expr5: 10K items, read 90%	

std::map では 2 threads 以上で
HLE on が上回る.
最大46%性能Up (3 threads)	

btree では傾向が安定しないが
概ね同程度と言える

27
Expr5: 10K items, read 99%	

std::map は最大 2.9 倍の性能
(3 threads)	

btree も最大 2.4 倍の性能
(3 threads)	

28
Expr5: 10K items, read 100%	

std::map は 6 threads で 7.4 倍
btree は 4 threads で 3.4 倍

29
Expr5: 1M items, read 0%	

3 threads 以上で HLE on の方が良い
std::map 6.9% up (3 threads)
btree: 7.3% up (3 threads)	

30
Expr5: 1M items, read 90%	

std::map は 58% up (3 threads)
btree は 54% up (3 threads)	

31
Expr5: 1M items, read 99%	

std::map は 2.4 倍 (3 threads)
btree は 2.4 倍 (3 threads)	

32
Expr5: 1M items read 100%	

std::map は 2.6 倍 (4 threads)
btree は 2.6 倍 (3 threads)	

33
Expr5: まとめ	
•  性能向上
–  10K items で 最大 7.4 倍 (read 100%)
–  1M items で 最大 2.6 倍 (read 100%)

•  現状の結論
–  データがキャッシュに乗る程度に小さく read 比
率が低いと HLE のオーバーヘッドが目立つ

•  考察
–  critical section でより多くの操作をするケースも
評価すべき 	
34
HLE 評価まとめ	
•  手間なしで性能が向上する魔法
–  楽観的挙動 à 悲観的挙動なので
性能最悪値を保証してくれるのも魅力
–  デッドロックは従来通り気をつける必要あり

•  使うべき条件
–  条件1: 競合が起きにくい (read 比率が高い)
–  条件2: クリティカルセクション実行時間が短い
–  条件3: アクセス対象メモリが少ない
35
今後の展望	
•  HLE には条件分岐予測のように elision すべきかど
うかを予測して性能向上させる余地があるのではな
いか?
–  今回あまり調べてないので既にやってたらごめんなさい

•  RTMは?
–  L1 のみならず,L2/L3 そしてメモリコントローラまで
TM のことを考えてくれるようになるまで様子見したい
–  速度が重要じゃない用途なら STM と連動できるように
なった時点で開発効率の点から有用なのではないか

36
おまけ: 私の単体 CPU 購入歴	
• 
• 
• 
• 
• 
• 

AMD K6-300
Intel Pentium II 333MHz
AMD Athlon 64 3200+
AMD Athlon X2 BE-2400
AMD Phenom II X4 910e
AMD Phenom II X6 1065T

•  こんな私が買う気になってるのだから
Haswell は凄い!	
37
実験してみたい人へ	
•  用意するもの
–  TSX サポート付きの Haswell CPU
–  Linux OS (古いものはオススメしない)
–  GCC 4.8 以降

•  ソースコード
–  https://github.com/starpos/hle_bench

38
ありがとうございました	
•  ご質問,コメントはご気軽にどうぞ J	

39

Intel TSX HLE を触ってみた x86opti

  • 1.
    Intel TSX HLEを触ってみた x86/x64最適化勉強会6 2013-08-31 星野喬@サイボウズ・ラボ / @starpoz 1
  • 2.
    自己紹介 •  星野 喬(@starpoz) – サイボウズ・ラボ •  興味 –  データベース,ストレージ,分散アルゴリズムなど •  今の仕事: #walbdev –  Linux における block device の効率的な 増分記録ドライバ •  http://developer.cybozu.co.jp/tech/?p=5130‎ 2
  • 3.
    今日の話 •  Intel の最新CPU (Haswell) の 新機能のひとつ TSX HLE を使い 性能評価をしてみて トランザクショナルメモリの可能性を探る •  後追い実験大歓迎 J 3
  • 4.
    トランザクショナルメモリ (TM) •  「IntelTSX について」参照 –  http://www.slideshare.net/starpos/intel-tsxx86opti4 •  要約: atomic メモリアクセスを実現する技術 –  他と競合したくない一連のメモリアクセス –  Lock-free な時代がすぐそこに(反論アリ 4
  • 5.
    Intel TSX •  TransactionalSynchronization eXtension •  2 つのインターフェースを提供 –  HLE: Lock prefix の拡張で細粒度の排他を実現 –  RTM: 制約はあるが HardwareTM そのもの •  楽観的な振舞 •  CPU L1 キャッシュ上で必要なデータを管理 •  キャッシュライン単位での競合検出 5
  • 6.
    競合 (collision) •  あるリソースへの複数主体からのアクセスが 同時に起きること(要出典) • Intel TSX の場合: –  リソース: キャッシュライン単位のメモリ領域 –  主体: スレッド –  アクセス: read/write (全て read の場合を除く) –  同時: クリティカルセクションの実行期間が重複 6
  • 7.
    HLE: Hardware LockElision 競合発生しないケース 競合発生するケース 時間 ロックを無視して 投機実行開始 競合発生しなかった めでたしめでたし 時間 競合発生 ロック取れたので 変更破棄 必ず実行できる ロック待ち体制 •  楽観的挙動 –(失敗)à 悲観的挙動 •  失敗すると電気代が無駄になる 7
  • 8.
    Spinlock with HLE • GCC 4.8 の場合 (マニュアル参照) class SpinlockHle { private: char &lock_; public: explicit SpinlockHle(char &lock) : lock_(lock) { int flags = __ATOMIC_ACQUIRE | __ATOMIC_HLE_ACQUIRE; while (__atomic_exchange_n(&lock_, 1, flags)) _mm_pause(); } ~SpinlockHle() noexcept { int flags = __ATOMIC_RELEASE | __ATOMIC_HLE_RELEASE; __atomic_clear(&lock_, flags); } }; 8
  • 9.
    HLE on/off の違い ---t1.hle0.s 2013-08-31 09:23:58.000000000 +0900 +++ t1.hle1.s 2013-08-31 09:23:30.000000000 +0900 @@ -13,12 +13,12 @@ .L3: rep nop .L2: movl %edx, %eax xchgb -1(%rsp), %al + xacquire xchgb -1(%rsp), %al testb %al, %al jne .L3 movb $0, -1(%rsp) + xrelease movb $0, -1(%rsp) xorl %eax, %eax ret .cfi_endproc .LFE859: 9
  • 10.
    実験環境 •  CPU: Corei7-4770 –  4cores 8HT –  HT 有効 –  TurboBoost 有効 •  メモリ: 16GB •  OS: Ubuntu 13.04 x86_64 kernel 3.8.19 •  コンパイラ: GCC 4.8.1 –  最適化フラグ: -O2 のみ 10
  • 11.
    実験方法 •  スレッドを必要なだけ起動して,X 秒間クリティカル セクション(CS) を繰り返し実行 •  Spinlock を使って CS の排他を取る –  HLE 有効/無効は実験パラメータ –  終了判定のために CS 実行毎に atomic<bool> を load するオーバーヘッドあり •  CS の実行回数の合計値を X で割ったものを スループットとする •  上記実験を Y 回行い,スループットの avg, min, max を計算 11
  • 12.
  • 13.
    Expr1: simple counter(s) • クリティカルセクションループ while (!isEnd_.load(std::memory_order_relaxed)) { SpinlockHle<useHLE> lk(mutex_); counter_++; } •  パラメータ –  競合率 100%: counter を全スレッドで共有 –  競合率 0%: スレッド毎に counter を持つ •  キャッシュラインが異なるように 64byte 毎に配置 Counter Thread 競合率 100% Thread 競合率 0% Counter 13
  • 14.
    Expr1: result 44.7x 10 sec,20 trials spinlock なしの 8 threads 実行で 4309M なので, isEnd.load() のオーバーヘッドは 8% 程度 14
  • 15.
    Expr1: result (scaled) 競合100%のときは オーバーヘッドの分 性能悪化 10 sec, 20 trials 15
  • 16.
    Expr2: counter(s) withdelay •  1us delay (誤差は実測 70ns 以下) void delayUsec(uint64_t usec) { auto ts0 = std::chrono::high_resolution_clock::now(); uint64_t diff = 0; while (diff < usec * 1000) { auto ts1 = std::chrono::high_resolution_clock::now(); diff = std::chrono::duration_cast< std::chrono::nanoseconds>(ts1 - ts0).count(); } } •  クリティカルセクション while (!isEnd_.load(std::memory_order_relaxed)) { SpinlockHle<useHLE> lk(mutex_); counter_++; delayUsec(1); } 16
  • 17.
    Expr2: result 10 sec,20 trials cs が 1us でも最大 5 倍程度の性能アップを期待できる 17
  • 18.
    Expr3: •  1usdelay の前/後にカウンタを更新する while (!isEnd_.load(std::memory_order_relaxed)) { SpinlockHle<useHLE> lk(mutex_); if (isBefore) delayUsec(1); counter_++; if (!isBefore) delayUsec(1); } •  目的 –  競合発覚するのが CS の最初か最後かで楽観的 実行が失敗する頻度が変化するのを観察したい 18
  • 19.
  • 20.
    Expr4: •  目的 – write buffer や read flag を管理する領域は有 限なので,HLE で恩恵が受けられるアクセスサイ ズの上限を知りたい •  手段 –  X 個の 64bytes メモリ断片をスレッド毎に用意 –  CS の中で Y 個にアクセスする(重複アリ) –  他の条件を同じにするため,Y は固定 –  2 <= X <= Y で評価 –  今回は write の評価のみ 20
  • 21.
    Expr4: 1-4 thread16 clines 1 threads 3 threads 10 sec, 20 trials 2 threads 4 threads CS 内では 12 lines までのアクセスにした方が良さそう 21
  • 22.
    Expr4: 5-8 thread16 clines 5 threads 7 threads 10 sec, 20 trials 6 threads 8 threads 安定しない結果.12 lines を越えても効果があるケースも 22
  • 23.
    Expr4: 5-8 thread64 clines 5 threads 7 threads 5 sec, 100 trials 6 threads 8 threads 安定しない結果,実験方法に問題があるかも? 23
  • 24.
    Expr5: •  目的: – 実用的なデータ構造で HLE の効果を知る •  手段: –  std::map<uint32_t, uint32_t> をひとつの spinlock で排他 –  read 比率を変える: 0%, 90%, 99%, 100% •  read 操作: ランダムキーで lower_bound 検索 •  write 操作: ひとつ削除,その後 insert –  初期アイテム数: 10K (約2MB), 1M (約100MB) –  (ついでに自作の btree map でも試す) 24
  • 25.
    20131022追記 •  以下 expr5の結果グラフのスループットは全 て誤って 3 倍に集計されていたことが発覚 –  集計スクリプトのミス •  スループットを見るときは表記の 1/3 にして ご覧ください 25
  • 26.
    Expr5: 10K items,read 0% std::map はスレッド数増加で HLE on が逆転 性能上昇は最大で 39% (8 threads) btree も同様の傾向 26
  • 27.
    Expr5: 10K items,read 90% std::map では 2 threads 以上で HLE on が上回る. 最大46%性能Up (3 threads) btree では傾向が安定しないが 概ね同程度と言える 27
  • 28.
    Expr5: 10K items,read 99% std::map は最大 2.9 倍の性能 (3 threads) btree も最大 2.4 倍の性能 (3 threads) 28
  • 29.
    Expr5: 10K items,read 100% std::map は 6 threads で 7.4 倍 btree は 4 threads で 3.4 倍 29
  • 30.
    Expr5: 1M items,read 0% 3 threads 以上で HLE on の方が良い std::map 6.9% up (3 threads) btree: 7.3% up (3 threads) 30
  • 31.
    Expr5: 1M items,read 90% std::map は 58% up (3 threads) btree は 54% up (3 threads) 31
  • 32.
    Expr5: 1M items,read 99% std::map は 2.4 倍 (3 threads) btree は 2.4 倍 (3 threads) 32
  • 33.
    Expr5: 1M itemsread 100% std::map は 2.6 倍 (4 threads) btree は 2.6 倍 (3 threads) 33
  • 34.
    Expr5: まとめ •  性能向上 – 10K items で 最大 7.4 倍 (read 100%) –  1M items で 最大 2.6 倍 (read 100%) •  現状の結論 –  データがキャッシュに乗る程度に小さく read 比 率が低いと HLE のオーバーヘッドが目立つ •  考察 –  critical section でより多くの操作をするケースも 評価すべき 34
  • 35.
    HLE 評価まとめ •  手間なしで性能が向上する魔法 – 楽観的挙動 à 悲観的挙動なので 性能最悪値を保証してくれるのも魅力 –  デッドロックは従来通り気をつける必要あり •  使うべき条件 –  条件1: 競合が起きにくい (read 比率が高い) –  条件2: クリティカルセクション実行時間が短い –  条件3: アクセス対象メモリが少ない 35
  • 36.
    今後の展望 •  HLE には条件分岐予測のようにelision すべきかど うかを予測して性能向上させる余地があるのではな いか? –  今回あまり調べてないので既にやってたらごめんなさい •  RTMは? –  L1 のみならず,L2/L3 そしてメモリコントローラまで TM のことを考えてくれるようになるまで様子見したい –  速度が重要じゃない用途なら STM と連動できるように なった時点で開発効率の点から有用なのではないか 36
  • 37.
    おまけ: 私の単体 CPU購入歴 •  •  •  •  •  •  AMD K6-300 Intel Pentium II 333MHz AMD Athlon 64 3200+ AMD Athlon X2 BE-2400 AMD Phenom II X4 910e AMD Phenom II X6 1065T •  こんな私が買う気になってるのだから Haswell は凄い! 37
  • 38.
    実験してみたい人へ •  用意するもの –  TSXサポート付きの Haswell CPU –  Linux OS (古いものはオススメしない) –  GCC 4.8 以降 •  ソースコード –  https://github.com/starpos/hle_bench 38
  • 39.