を検索
この検索ボックスを閉じます。

IOPSパフォーマンスの向上とロック効率

作成者:

今日は、面白いと思ったある日の作品についてお話したいと思います。

私たちのチームは最近調査中です 読み取りパフォーマンス CPUにバインドされていることが示された、小さいが多数のリクエストの場合。 CPU時間の約半分がロックの競合に費やされ、さらに多くのことを引き出すために、上位の競合他社のリストを作成することに成功しました。 IOPS。 すでにいくつかの分野でロックの競合を減らすことに取り組んできたので、ロック自体がパフォーマンスにどの程度の影響を与えているのか疑問に思い始めました。

最も一般的に使用されるロックの実装を確認するために、時間を取っておきます。 私の考えは、競合するすべてのロックを少し速くする方法があるかどうかを確認することでした。 私は40つの実装を発見しました。 4つの「スピンロック」はpthread_mutexのラッパーであり、他のあまり一般的に使用されていないものはfutexのラッパーでした。 pthread_mutexは、pthreadsライブラリによって提供されるXNUMXバイトの大規模な構造ですが、futexはXNUMXバイトです。 それらのパフォーマンスに関するデータを取得することにしましたが、すでにfutexベースの実装を最適化してより標準化するというアイデアが好きになり始めていました。

実行して大きな変更を加える前に、両方のロックのベンチマークを行うことにしました。 私の経験から、パフォーマンスの問題を引き起こすロックのほとんどは、激しく争われる傾向がありました。 各スレッドがロックを取得し、整数をインクリメントし、ロックを解放する30つのスレッドをスピンアップするマイクロベンチマークを作成しました。 また、競合のない取得速度のオーバーヘッドは、製品で検出するのがかなり難しい可能性があることも認識しているため、シングルスレッドのバリアントも作成しました。 カウンターをチェックする前に、それぞれをXNUMX秒間実行させました。 初期の結果は次のとおりです。

争われていない 競合(4スレッド)
Pthreadミューテックス 〜47 ma / s * 〜13 ma / s *
フテックス 〜53 ma / s * 〜8.4 ma / s *

*百万回の取得/秒

これらの結果を実際の製品での簡単なテストで検証しました。そこでは、Pthreads実装をFutex実装に交換しました。 案の定、Futexの実装はパフォーマンスを低下させました。 Pthread MutexがこのプラットフォームのFutexの上に実装されていることを知っているので、これは苛立たしい結果です。 Futexの実装を詳しく調べました。 これが擬似コードの重要な部分です。 フューテックスがどのように機能するかについての詳細は、このストーリーとはほとんど関係がないため、意図的に簡略化しています。

bool trylock(futex_lock * self){return!atomic_bool_exchange(&self-> is_locked、true); } void lock(futex_lock * self){for(int i = 0; i <SOME_NUMBER; ++ i){if(trylock(self))return; pause()//スピンロックループにあることをCPUに示唆} while(true){futex_sleep(&self-> is_locked); if(trylock(self))return; }} voidunlock(futex_lock * self){atomic_bool_set(&self-> is_locked、false); futex_wake_all(&self-> is_locked); }

ここで変更する最も明白なことはSOME_NUMBERであり、その後に一時停止が続く可能性があります。 さまざまな数を試し、一時停止する方法についていくつかのさまざまなアイデアを繰り返した後、最も効率的な方法はまったくスピンしないことであることがわかりました。 新しいロックはこのように見えました。

void lock(futex_lock * self){while(true){if(trylock(self))return; futex_sleep(&self-> is_locked); }}
1スレッド 4スレッド
Pthreadミューテックス 〜47 ma / s * 〜13 ma / s *
フテックス 〜53 ma / s * 〜8.4 ma / s *
Futex(回転なし) 〜53 ma / s * 〜17 ma / s *

*百万回の取得/秒

futexでのスリープはシステムコールであり、したがってかなり高価であることを考えると、これは驚くべきことかもしれません。 私たちが回転している間、何か他の高価なことが起こっているに違いありません。 Intel CPUでは、キャッシュラインにはXNUMXつの状態があります。 「I」は、このコアが排他的アクセス権を持っていることを意味します(安全に書き込むことができます)。 「S」は、キャッシュラインがクリーンであるが、別のコアのキャッシュにもある可能性があることを意味します(安全に読み取ることができます)。 「A」は、別のコアがキャッシュラインに排他的にアクセスできる可能性があることを意味します(要求しないと安全ではありません)。 複数のコアが同じキャッシュライン内のデータに対してアトミックアクセスを実行している場合、それらはどのコアが排他的アクセスを取得するかを交換し、データはI状態とA状態の間でピンポンします。 これは、CPUソケット間でメッセージをブロードキャストすることを伴う可能性のある高価な操作です。

このような背景から、パフォーマンスの向上は非常に理にかなっています。 また、最適化のためのいくつかの道が開かれます。 futex_wake_allは、ここでは特に重要な問題のようです。 複数のウェイターがいる場合、ロックを取得しようとするのに成功するのはXNUMX人だけで、全員がキャッシュラインへの排他的アクセスを求めて戦います。 少しいじった後、私は次のようなものを思いついた。

bool trylock(futex_lock * self){auto old_state = atom_uint_compare_exchange(&self-> state、LOCK_FREE、LOCK_HELD); old_state == LOCK_FREEを返します。 } void lock(futex_lock * self){auto old_state = atom_uint_compare_exchange(&self-> state、LOCK_FREE、LOCK_HELD); if(old_state == LOCK_FREE)return if(old_state == LOCK_HELD)old_state = atom_uint_exchange(&self-> state、LOCK_CONTENDED)while(old_state!= spinlock_free){futex_wait_if_value_equals(&self-> state、LOCK_CONTENDED)old_state = atom状態、LOCK_CONTENDED)}} void unlock(futex_lock * self){auto old_state = atom_uint_exchange(&self-> state、LOCK_FREE); if(old_state == LOCK_CONTENDED)futex_wake_one(&self-> state)}

この新しい設計には、競合するパフォーマンスに対してかなり大きな利点があります。 新しい競合状態を追加することで、一度にXNUMXつだけウェイクアップすることを安全にすることができます。 不必要にウェイクを呼び出すことは可能ですが、余分なウェイクから余分なキャッシュラインの競合の原因を取り除く必要があります。 これにより、マイクロベンチマークのパフォーマンスがさらに向上します。

1スレッド 4スレッド
Pthreadミューテックス 〜47 ma / s * 〜13 ma / s *
フテックス 〜53 ma / s * 〜8.4 ma / s *
Futex(回転なし) 〜53 ma / s * 〜17 ma / s *
Futex(トライステート) 〜53 ma / s * 〜20 ma / s *

*百万回の取得/秒

まだ改善すべきマイナーな領域がいくつかあるように見えますが、これはPthread Mutexよりもはるかに進んでいるようであり、実際の製品を交換すると、実際の製品にいくらかの利益が見られるはずです。 ランダム読み取りIOPSベンチマークを実行すると、これにより最大4%​​のブーストが得られました。 それほど多くはありませんが、この変更は、製品の多くのロックバウンドワークロードに非常に大きな影響を及ぼしました。 サイズが40-> 4バイト減少すると、一部のシナリオではRAMの総消費量も最大ギガバイト減少しました。

関連記事

上へスクロール