Upgrade to Pro — share decks privately, control downloads, hide ads and more …

L1キャッシュ怖い

 L1キャッシュ怖い

RadixSortを並列化&高速化する話

Avatar for Hiroki KUMAZAKI

Hiroki KUMAZAKI

April 10, 2023

Other Decks in Programming

Transcript

  1. Radix Sortとは • O(N)なソートアルゴリズム – iwiwi博士「経験上実測で一番速い」 • O(N)のメモリ領域が追加で必要になる – なので多くの場面ではMerge

    SortやQuick Sortが 採用されたりする。 – In-PlaceなRadix Sortは最近の研究トピックの一つ • 実装上内部でCounting Sortを使うことが多い – CountingSortに関しては次ページ
  2. Counting Sortとは 2 3 3 5 9 4 3 3

    9 4 2 3 1 1 4 7 1 2 3 4 5 7 9 2 2 5 3 1 1 2 1. 数える 2 2 5 3 1 1 2 2. マッピング 1 1 2 2 3 3 3 3 3 4 4 4 5 7 9 9 3. 埋める
  3. Radix Sort with Counting Sort • CountingSortは値のバリエーションが増えると 死ぬ • 実用上8bit(256種)ずつCounting

    Sortする。 • Counting SortはStableなので下8bitから順に Sortする(Least Significant Digit first)。 • 上8bitからSortする事も可能(Most Significant Digit first)こっちはまた後で。
  4. LSD Radix Sort • わかりやすいように10進数で 313 210 118 410 123

    471 292 384 313 210 118 410 123 471 292 384 313 210 118 410 123 471 292 384 313 210 118 410 123 471 292 384 1の位でCountSort 10の位でCountSort 100の位でCountSort
  5. iwiwi sortの基本戦略 • LSD Radix Sortを並列化している – Software Write Combiningというテクニックで高速化

    – 基本的に結構素直な実装 • uint64_tなら、8回のCountSortを行う – データ全体を舐めてカウント – データ全体を隣のバッファにまるっと並べ替え移動 – 上記2ステップを8回繰り返す
  6. SoftWare Write Combining • CPUが書き出すアドレスを絞って連続させて 溜めてから書き出す事でWriteBufferをギチギ チに埋める実装技法。 CPU キャッシュ メモリ

    WriteBuffer 書き込み先アドレスが次々変わると WriteBufferが埋まり切る前に書き出されて リソースが無駄になる
  7. SoftWare Write Combining • CPUが書き出すアドレスを絞って連続させて 溜めてから書き出す事でWriteBufferをギチギ チに埋める実装技法。 CPU キャッシュ メモリ

    WriteBuffer WriteBufferが埋まり切る単位でしか キャッシュから書き出さないようにする。 データの移動回数の総量は倍に増えてる のに何故か高速化する。L1キャッシュ怖い。
  8. Radix Sort with SWWC • Countingフェーズは変わらない • データ移動フェーズで移動先候補が256箇所 あるのでそれを一旦ローカルの配列に書き込 んで、キャッシュライン長になったらmemcpy().

    for (size_t j = 0; j < size; ++j) { uint8_t idx = (from[j] >> i) &255); wbuffer[idx][wbuff_n[idx]++] = from[j]; if (wbuff_n[idx] == 8) { // cache line memcpy(&to[offsets[idx]], &wbuffer[idx], 64); offsets[idx] += 8; wbuff_n[idx] = 0; } } for (size_t j = 0; j < size; ++j) { uint8_t idx = (from[j] >> i) &255); to[offsets[idx]++] = from[j]; }
  9. MSD Radix Sort • わかりやすいように10進数で 313 210 118 410 123

    471 292 384 313 210 118 410 123 471 292 384 313 210 118 410 123 471 292 384 313 210 118 410 123 471 292 384 100の位でCountSort 10の位でCountSort 1の位でCountSort
  10. 最初だけMSD Radix Sort • MSDで256バケットに分けた段階でCPUソケッ トを跨るように設計。跨った後は1バケット毎 に1CPUコアが専属で張り付いてLSD Radix Sortする •

    LSD Radix Sortの際にCountは一度に可能 for (int j = 0; j < size; ++j) { for (size_t i = 0; i < 7; ++i) { uint8_t base = i * 8; ++l_offsets[i][data[j] >> base & 255]; } } // l_offsets[0:7]を使って7回並び替え for (int i = 0; i < 7; ++i) { for (int j = 0; j < size; ++j) { uint8_t base = i * 8; ++offsets[data[j]>> base & 255]; } // offsetsを使って並び替え }
  11. 何この差? • キャッシュラインにいくつ溜まったら書き出し たか、の差 for (size_t j = 0; j

    < size; ++j) { uint8_t idx = (from[j] >> i) &255); wbuffer[idx][wbuff_n[idx]++] = from[j]; if (wbuff_n[idx] == 8) { // cache line memcpy(&to[offsets[idx]], &wbuffer[idx], 64); offsets[idx] += 8; wbuff_n[idx] = 0; } } for (size_t j = 0; j < size; ++j) { uint8_t idx = (from[j] >> i) &255); wbuffer[idx][wbuff_n[idx]++] = from[j]; if (wbuff_n[idx] == 16) { // cache line memcpy(&to[offsets[idx]], &wbuffer[idx], 128); offsets[idx] += 8; wbuff_n[idx] = 0; } } msd8 msd16
  12. 考察 • 64byteを256本使うのがmsd8 – 64*256=16KiB ← つまりL1-Dの半分を使う – もう半分はソート対象データのprefetchで適当に 上手く使われるのでは?(当初の思惑)

    • 128byteを256本使うmsd16の方が1割程速い – 128*256=32KiB ← L1-Dを全部使う – よくわからないがワンショットで読むぐらいなら キャッシュを気にする事はない?
  13. 考察 • msd32とmsd64が最速を奪い合う構図 – お前らL1-Dキャッシュから溢れてるやんけ!! • 助けてVTune先生! • VTune先生「if文が足引っ張ってるやで」 for

    (size_t j = 0; j < size; ++j) { uint8_t idx = (from[j] >> i) &255); wbuffer[idx][wbuff_n[idx]++] = from[j]; if (wbuff_n[idx] == 16) { // ここの分岐予測は1/16の確率で外れる memcpy(&to[offsets[idx]], &wbuffer[idx], 128); offsets[idx] += 8; wbuff_n[idx] = 0; } }