なぜ私の Intel Skylake / Kaby Lake CPU は、単純なハッシュ テーブルの実装で不可解な 3 倍の速度低下を起こすのでしょうか? 質問する

なぜ私の Intel Skylake / Kaby Lake CPU は、単純なハッシュ テーブルの実装で不可解な 3 倍の速度低下を起こすのでしょうか? 質問する

要するに:

キャッシュラインにぴったり合うバケット (複数の要素を含む) を持つシンプルな (マルチキー) ハッシュ テーブルを実装しました。キャッシュライン バケットへの挿入は非常に簡単で、メイン ループの重要な部分です。

同じ結果を生成し、同じように動作する 3 つのバージョンを実装しました。

推理小説

しかし、すべてのバージョンでキャッシュライン アクセス パターンがまったく同じで、ハッシュ テーブル データも同一であるにもかかわらず、パフォーマンスに驚くほど大きな 3 倍の違いが見られます。

最良の実装は、私の CPU (i7-7700HQ) 上で&insert_okと比較して約 3 倍の速度低下を招きます。1 つのバリアント insert_badは、書き込み先の位置 (既にわかっている) を見つけるためにキャッシュライン内で余分な不要な線形検索を追加する、 の単純な変更であり、この 3 倍の速度低下は発生しません。insert_badinsert_altinsert_ok

まったく同じ実行ファイルは、他の CPU (AMD 5950X (Zen 3)、Intel i7-11800H (Tiger Lake))とinsert_ok比較して 1.6 倍高速であることが示されています。insert_badinsert_alt

# see https://github.com/cr-marcstevens/hashtable_mystery
$ ./test.sh
model name      : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
==============================
CXX=g++    CXXFLAGS=-std=c++11 -O2 -march=native -falign-functions=64
tablesize: 117440512 elements: 67108864 loadfactor=0.571429
- test insert_ok : 11200ms
- test insert_bad: 3164ms
  (outcome identical to insert_ok: true)
- test insert_alt: 3366ms
  (outcome identical to insert_ok: true)

tablesize: 117440813 elements: 67108864 loadfactor=0.571427
- test insert_ok : 10840ms
- test insert_bad: 3301ms
  (outcome identical to insert_ok: true)
- test insert_alt: 3579ms
  (outcome identical to insert_ok: true)

コード

// insert element in hash_table
inline void insert_ok(uint64_t k)
{
    // compute target bucket
    uint64_t b = mod(k);
    // bounded linear search for first non-full bucket
    for (size_t c = 0; c < 1024; ++c)
    {
        bucket_t& B = table_ok[b];
        // if bucket non-full then store element and return
        if (B.size != bucket_size)
        {
            B.keys[B.size] = k;
            B.values[B.size] = 1;
            ++B.size;
            ++table_count;
            return;
        }
        // increase b w/ wrap around
        if (++b == table_size)
            b = 0;
    }
}
// equivalent to insert_ok
// but uses a stupid linear search to store the element at the target position
inline void insert_bad(uint64_t k)
{
    // compute target bucket
    uint64_t b = mod(k);
    // bounded linear search for first non-full bucket
    for (size_t c = 0; c < 1024; ++c)
    {
        bucket_t& B = table_bad[b];
        // if bucket non-full then store element and return
        if (B.size != bucket_size)
        {
            for (size_t i = 0; i < bucket_size; ++i)
            {
                if (i == B.size)
                {
                    B.keys[i] = k;
                    B.values[i] = 1;
                    ++B.size;
                    ++table_count;
                    return;
                }
            }
        }
        // increase b w/ wrap around
        if (++b == table_size)
            b = 0;
    }
}
// instead of using bucket_t.size, empty elements are marked by special empty_key value
// a bucket is filled first to last, so bucket is full if last element key != empty_key
uint64_t empty_key = ~uint64_t(0);
inline void insert_alt(uint64_t k)
{
    // compute target bucket
    uint64_t b = mod(k);
    // bounded linear search for first non-full bucket
    for (size_t c = 0; c < 1024; ++c)
    {
        bucket_t& B = table_alt[b];
        // if bucket non-full then store element and return
        if (B.keys[bucket_size-1] == empty_key)
        {
            for (size_t i = 0; i < bucket_size; ++i)
            {
                if (B.keys[i] == empty_key)
                {
                    B.keys[i] = k;
                    B.values[i] = 1;
                    ++table_count;
                    return;
                }
            }
        }
        // increase b w/ wrap around
        if (++b == table_size)
            b = 0;
    }
}

私の分析

ループ C++ にさまざまな変更を加えようとしましたが、本質的に非常に単純なため、コンパイラは同じアセンブリを生成します。結果のアセンブリからは、係数 3 の損失が何を引き起こすかは実際には明らかではありません。perf で測定してみましたが、意味のある違いを特定できないようです。

いずれも比較的小さなループである 3 つのバージョンのアセンブリを比較すると、これらのバージョン間で 3 倍の損失を引き起こす可能性のあるものは何も見つかりません。

したがって、3 倍の速度低下は、自動プリフェッチ、分岐予測、命令/ジャンプ アライメント、またはそれらの組み合わせによる奇妙な影響であると考えられます。

ここで実際にどのような影響が及んでいるかを測定するための、より良い洞察や方法を持っている人はいますか?

詳細

この問題を説明する小さなC++11の例を作成しました。コードは次の場所にあります。https://github.com/cr-marcstevens/hashtable_mystery

これには、私の CPU でこの問題が発生することを示す独自の静的バイナリも含まれます。コンパイラが異なると、異なるコードが生成される可能性があるためです。また、3 つのハッシュ テーブル バージョンすべてのアセンブリ コードをダンプしました。

パフォーマンスイベント測定

ここに、パフォーマンス イベントの測定値が多数あります。ここでは、missと という単語を含むものに焦点を当てましたstall。各イベントには 2 行あります。

  • 最初の行はinsert_ok減速している部分に対応する
  • 2行目は、insert_alt追加のループと追加の作業があるが、より高速になる。
=== L1-dcache-load-misses ===
insert_ok : 171411476
insert_alt: 244244027
=== L1-dcache-loads ===
insert_ok : 775468123
insert_alt: 1038574743
=== L1-dcache-stores ===
insert_ok : 621353009
insert_alt: 554244145
=== L1-icache-load-misses ===
insert_ok : 69666
insert_alt: 259102
=== LLC-load-misses ===
insert_ok : 70519701
insert_alt: 71399242
=== LLC-loads ===
insert_ok : 130909270
insert_alt: 134776189
=== LLC-store-misses ===
insert_ok : 16782747
insert_alt: 16851787
=== LLC-stores ===
insert_ok : 17072141
insert_alt: 17534866
=== arith.divider_active ===
insert_ok : 26810
insert_alt: 26611
=== baclears.any ===
insert_ok : 2038060
insert_alt: 7648128
=== br_inst_retired.all_branches ===
insert_ok : 546479449
insert_alt: 938434022
=== br_inst_retired.all_branches_pebs ===
insert_ok : 546480454
insert_alt: 938412921
=== br_inst_retired.cond_ntaken ===
insert_ok : 237470651
insert_alt: 433439086
=== br_inst_retired.conditional ===
insert_ok : 477604946
insert_alt: 802468807
=== br_inst_retired.far_branch ===
insert_ok : 1058138
insert_alt: 1052510
=== br_inst_retired.near_call ===
insert_ok : 227076
insert_alt: 227074
=== br_inst_retired.near_return ===
insert_ok : 227072
insert_alt: 227070
=== br_inst_retired.near_taken ===
insert_ok : 307946256
insert_alt: 503926433
=== br_inst_retired.not_taken ===
insert_ok : 237458763
insert_alt: 433429466
=== br_misp_retired.all_branches ===
insert_ok : 36443541
insert_alt: 90626754
=== br_misp_retired.all_branches_pebs ===
insert_ok : 36441027
insert_alt: 90622375
=== br_misp_retired.conditional ===
insert_ok : 36454196
insert_alt: 90591031
=== br_misp_retired.near_call ===
insert_ok : 173
insert_alt: 169
=== br_misp_retired.near_taken ===
insert_ok : 19032467
insert_alt: 40361420
=== branch-instructions ===
insert_ok : 546476228
insert_alt: 938447476
=== branch-load-misses ===
insert_ok : 36441314
insert_alt: 90611299
=== branch-loads ===
insert_ok : 546472151
insert_alt: 938435143
=== branch-misses ===
insert_ok : 36436325
insert_alt: 90597372
=== bus-cycles ===
insert_ok : 222283508
insert_alt: 88243938
=== cache-misses ===
insert_ok : 257067753
insert_alt: 475091979
=== cache-references ===
insert_ok : 445465943
insert_alt: 590770464
=== cpu-clock ===
insert_ok : 10333.94 msec cpu-clock:u # 1.000 CPUs utilized
insert_alt: 4766.53 msec cpu-clock:u # 1.000 CPUs utilized
=== cpu-cycles ===
insert_ok : 25273361574
insert_alt: 11675804743
=== cpu_clk_thread_unhalted.one_thread_active ===
insert_ok : 223196489
insert_alt: 88616919
=== cpu_clk_thread_unhalted.ref_xclk ===
insert_ok : 222719013
insert_alt: 88467292
=== cpu_clk_unhalted.one_thread_active ===
insert_ok : 223380608
insert_alt: 88212476
=== cpu_clk_unhalted.ref_tsc ===
insert_ok : 32663820508
insert_alt: 12901195392
=== cpu_clk_unhalted.ref_xclk ===
insert_ok : 221957996
insert_alt: 88390991
insert_alt: === cpu_clk_unhalted.ring0_trans ===
insert_ok : 374
insert_alt: 373
=== cpu_clk_unhalted.thread ===
insert_ok : 25286801620
insert_alt: 11714137483
=== cycle_activity.cycles_l1d_miss ===
insert_ok : 16278956219
insert_alt: 7417877493
=== cycle_activity.cycles_l2_miss ===
insert_ok : 15607833569
insert_alt: 7054717199
=== cycle_activity.cycles_l3_miss ===
insert_ok : 12987627072
insert_alt: 6745771672
=== cycle_activity.cycles_mem_any ===
insert_ok : 23440206343
insert_alt: 9027220495
=== cycle_activity.stalls_l1d_miss ===
insert_ok : 16194872307
insert_alt: 4718344050
=== cycle_activity.stalls_l2_miss ===
insert_ok : 15350067722
insert_alt: 4578933898
=== cycle_activity.stalls_l3_miss ===
insert_ok : 12697354271
insert_alt: 4457980047
=== cycle_activity.stalls_mem_any ===
insert_ok : 20930005455
insert_alt: 4555461595
=== cycle_activity.stalls_total ===
insert_ok : 22243173394
insert_alt: 6561416461
=== dTLB-load-misses ===
insert_ok : 67817362
insert_alt: 63603879
=== dTLB-loads ===
insert_ok : 775467642
insert_alt: 1038562488
=== dTLB-store-misses ===
insert_ok : 8823481
insert_alt: 13050341
=== dTLB-stores ===
insert_ok : 621353007
insert_alt: 554244145
=== dsb2mite_switches.count ===
insert_ok : 93894397
insert_alt: 315793354
=== dsb2mite_switches.penalty_cycles ===
insert_ok : 9216240937
insert_alt: 206393788
=== dtlb_load_misses.miss_causes_a_walk ===
insert_ok : 177266866
insert_alt: 101439773
=== dtlb_load_misses.stlb_hit ===
insert_ok : 2994329
insert_alt: 35601646
=== dtlb_load_misses.walk_active ===
insert_ok : 4747616986
insert_alt: 3893609232
=== dtlb_load_misses.walk_completed ===
insert_ok : 67817832
insert_alt: 63591832
=== dtlb_load_misses.walk_completed_4k ===
insert_ok : 67817841
insert_alt: 63596148
=== dtlb_load_misses.walk_pending ===
insert_ok : 6495600072
insert_alt: 5987182579
=== dtlb_store_misses.miss_causes_a_walk ===
insert_ok : 89895924
insert_alt: 21841494
=== dtlb_store_misses.stlb_hit ===
insert_ok : 4940907
insert_alt: 21970231
=== dtlb_store_misses.walk_active ===
insert_ok : 1784142210
insert_alt: 903334856
=== dtlb_store_misses.walk_completed ===
insert_ok : 8845884
insert_alt: 13071262
=== dtlb_store_misses.walk_completed_4k ===
insert_ok : 8822993
insert_alt: 12936414
=== dtlb_store_misses.walk_pending ===
insert_ok : 1842905733
insert_alt: 933039119
=== exe_activity.1_ports_util ===
insert_ok : 991400575
insert_alt: 1433908710
=== exe_activity.2_ports_util ===
insert_ok : 782270731
insert_alt: 1314443071
=== exe_activity.3_ports_util ===
insert_ok : 556847358
insert_alt: 1158115803
=== exe_activity.4_ports_util ===
insert_ok : 427323800
insert_alt: 783571280
=== exe_activity.bound_on_stores ===
insert_ok : 299732094
insert_alt: 303475333
=== exe_activity.exe_bound_0_ports ===
insert_ok : 227569792
insert_alt: 348959512
=== frontend_retired.dsb_miss ===
insert_ok : 6771584
insert_alt: 93700643
=== frontend_retired.itlb_miss ===
insert_ok : 1115
insert_alt: 1689
=== frontend_retired.l1i_miss ===
insert_ok : 3639
insert_alt: 3857
=== frontend_retired.l2_miss ===
insert_ok : 2826
insert_alt: 2830
=== frontend_retired.latency_ge_1 ===
insert_ok : 9206268
insert_alt: 178345368
=== frontend_retired.latency_ge_128 ===
insert_ok : 2708
insert_alt: 2703
=== frontend_retired.latency_ge_16 ===
insert_ok : 403492
insert_alt: 820950
=== frontend_retired.latency_ge_2 ===
insert_ok : 4981263
insert_alt: 85781924
=== frontend_retired.latency_ge_256 ===
insert_ok : 802
insert_alt: 970
=== frontend_retired.latency_ge_2_bubbles_ge_1 ===
insert_ok : 56936702
insert_alt: 225712704
=== frontend_retired.latency_ge_2_bubbles_ge_2 ===
insert_ok : 10312026
insert_alt: 163227996
=== frontend_retired.latency_ge_2_bubbles_ge_3 ===
insert_ok : 7599252
insert_alt: 122841752
=== frontend_retired.latency_ge_32 ===
insert_ok : 3599
insert_alt: 3317
=== frontend_retired.latency_ge_4 ===
insert_ok : 2627373
insert_alt: 42287077
=== frontend_retired.latency_ge_512 ===
insert_ok : 418
insert_alt: 241
=== frontend_retired.latency_ge_64 ===
insert_ok : 2474
insert_alt: 2802
=== frontend_retired.latency_ge_8 ===
insert_ok : 528748
insert_alt: 951836
=== frontend_retired.stlb_miss ===
insert_ok : 769
insert_alt: 562
=== hw_interrupts.received ===
insert_ok : 9330
insert_alt: 3738
=== iTLB-load-misses ===
insert_ok : 456094
insert_alt: 90739
=== iTLB-loads ===
insert_ok : 949
insert_alt: 1031
=== icache_16b.ifdata_stall ===
insert_ok : 1145821
insert_alt: 862403
=== icache_64b.iftag_hit ===
insert_ok : 1378406022
insert_alt: 4459469241
=== icache_64b.iftag_miss ===
insert_ok : 61812
insert_alt: 57204
=== icache_64b.iftag_stall ===
insert_ok : 56551468
insert_alt: 82354039
=== idq.all_dsb_cycles_4_uops ===
insert_ok : 896374829
insert_alt: 1610100578
=== idq.all_dsb_cycles_any_uops ===
insert_ok : 1217878089
insert_alt: 2739912727
=== idq.all_mite_cycles_4_uops ===
insert_ok : 315979501
insert_alt: 480165021
=== idq.all_mite_cycles_any_uops ===
insert_ok : 1053703958
insert_alt: 2251382760
=== idq.dsb_cycles ===
insert_ok : 1218891711
insert_alt: 2744099964
=== idq.dsb_uops ===
insert_ok : 5828442701
insert_alt: 10445095004
=== idq.mite_cycles ===
insert_ok : 470409312
insert_alt: 1664892371
=== idq.mite_uops ===
insert_ok : 1407396065
insert_alt: 4515396737
=== idq.ms_cycles ===
insert_ok : 583601361
insert_alt: 587996351
=== idq.ms_dsb_cycles ===
insert_ok : 218346
insert_alt: 74155
=== idq.ms_mite_uops ===
insert_ok : 1266443204
insert_alt: 1277980465
=== idq.ms_switches ===
insert_ok : 149106449
insert_alt: 150392336
=== idq.ms_uops ===
insert_ok : 1266950097
insert_alt: 1277330690
=== idq_uops_not_delivered.core ===
insert_ok : 1871959581
insert_alt: 6531069387
=== idq_uops_not_delivered.cycles_0_uops_deliv.core ===
insert_ok : 289301660
insert_alt: 946930713
=== idq_uops_not_delivered.cycles_fe_was_ok ===
insert_ok : 24668869613
insert_alt: 9335642949
=== idq_uops_not_delivered.cycles_le_1_uop_deliv.core ===
insert_ok : 393750384
insert_alt: 1344106460
=== idq_uops_not_delivered.cycles_le_2_uop_deliv.core ===
insert_ok : 506090534
insert_alt: 1824690188
=== idq_uops_not_delivered.cycles_le_3_uop_deliv.core ===
insert_ok : 688462029
insert_alt: 2416339045
=== ild_stall.lcp ===
insert_ok : 380
insert_alt: 480
=== inst_retired.any ===
insert_ok : 4760842560
insert_alt: 5470438932
=== inst_retired.any_p ===
insert_ok : 4760919037
insert_alt: 5470404264
=== inst_retired.prec_dist ===
insert_ok : 4760801654
insert_alt: 5470649220
=== inst_retired.total_cycles_ps ===
insert_ok : 25175372339
insert_alt: 11718929626
=== instructions ===
insert_ok : 4760805219
insert_alt: 5470497783
=== int_misc.clear_resteer_cycles ===
insert_ok : 199623562
insert_alt: 671083279
=== int_misc.recovery_cycles ===
insert_ok : 314434729
insert_alt: 704406698
=== itlb.itlb_flush ===
insert_ok : 303
insert_alt: 248
=== itlb_misses.miss_causes_a_walk ===
insert_ok : 19537
insert_alt: 116729
=== itlb_misses.stlb_hit ===
insert_ok : 11323
insert_alt: 5557
=== itlb_misses.walk_active ===
insert_ok : 2809766
insert_alt: 4070194
=== itlb_misses.walk_completed ===
insert_ok : 24298
insert_alt: 45251
=== itlb_misses.walk_completed_4k ===
insert_ok : 34084
insert_alt: 29759
=== itlb_misses.walk_pending ===
insert_ok : 853764
insert_alt: 2817933
=== l1d.replacement ===
insert_ok : 171135334
insert_alt: 244967326
=== l1d_pend_miss.fb_full ===
insert_ok : 354631656
insert_alt: 382309583
=== l1d_pend_miss.pending ===
insert_ok : 16792436441
insert_alt: 22979721104
=== l1d_pend_miss.pending_cycles ===
insert_ok : 16377420892
insert_alt: 7349245429
=== l1d_pend_miss.pending_cycles_any ===
insert_ok : insert_alt: === l2_lines_in.all ===
insert_ok : 303009088
insert_alt: 411750486
=== l2_lines_out.non_silent ===
insert_ok : 157208112
insert_alt: 309484666
=== l2_lines_out.silent ===
insert_ok : 127379047
insert_alt: 84169481
=== l2_lines_out.useless_hwpf ===
insert_ok : 70374658
insert_alt: 144359127
=== l2_lines_out.useless_pref ===
insert_ok : 70747103
insert_alt: 142931540
=== l2_rqsts.all_code_rd ===
insert_ok : 71254
insert_alt: 242327
=== l2_rqsts.all_demand_data_rd ===
insert_ok : 137366274
insert_alt: 143507049
=== l2_rqsts.all_demand_miss ===
insert_ok : 150071420
insert_alt: 150820168
=== l2_rqsts.all_demand_references ===
insert_ok : 154854022
insert_alt: 160487082
=== l2_rqsts.all_pf ===
insert_ok : 170261458
insert_alt: 282476184
=== l2_rqsts.all_rfo ===
insert_ok : 17575896
insert_alt: 16938897
=== l2_rqsts.code_rd_hit ===
insert_ok : 79800
insert_alt: 381566
=== l2_rqsts.code_rd_miss ===
insert_ok : 25800
insert_alt: 33755
=== l2_rqsts.demand_data_rd_hit ===
insert_ok : 5191029
insert_alt: 9831101
=== l2_rqsts.demand_data_rd_miss ===
insert_ok : 132253891
insert_alt: 133965310
=== l2_rqsts.miss ===
insert_ok : 305347974
insert_alt: 414758839
=== l2_rqsts.pf_hit ===
insert_ok : 14639778
insert_alt: 19484420
=== l2_rqsts.pf_miss ===
insert_ok : 156092998
insert_alt: 263293430
=== l2_rqsts.references ===
insert_ok : 326549998
insert_alt: 443460029
=== l2_rqsts.rfo_hit ===
insert_ok : 11650
insert_alt: 21474
=== l2_rqsts.rfo_miss ===
insert_ok : 17544467
insert_alt: 16835137
=== l2_trans.l2_wb ===
insert_ok : 157044674
insert_alt: 308107712
=== ld_blocks.no_sr ===
insert_ok : 14
insert_alt: 13
=== ld_blocks.store_forward ===
insert_ok : 158
insert_alt: 128
=== ld_blocks_partial.address_alias ===
insert_ok : 5155853
insert_alt: 17867414
=== load_hit_pre.sw_pf ===
insert_ok : 10840795
insert_alt: 11072297
=== longest_lat_cache.miss ===
insert_ok : 257061118
insert_alt: 471152073
=== longest_lat_cache.reference ===
insert_ok : 445701577
insert_alt: 583870610
=== machine_clears.count ===
insert_ok : 3926377
insert_alt: 4280080
=== machine_clears.memory_ordering ===
insert_ok : 97177
insert_alt: 25407
=== machine_clears.smc ===
insert_ok : 138579
insert_alt: 305423
=== mem-stores ===
insert_ok : 621353009
insert_alt: 554244143
=== mem_inst_retired.all_loads ===
insert_ok : 775473590
insert_alt: 1038559807
=== mem_inst_retired.all_stores ===
insert_ok : 621353013
insert_alt: 554244145
=== mem_inst_retired.lock_loads ===
insert_ok : 85
insert_alt: 85
=== mem_inst_retired.split_loads ===
insert_ok : 171
insert_alt: 174
=== mem_inst_retired.split_stores ===
insert_ok : 53
insert_alt: 49
=== mem_inst_retired.stlb_miss_loads ===
insert_ok : 68308539
insert_alt: 18088047
=== mem_inst_retired.stlb_miss_stores ===
insert_ok : 264054
insert_alt: 819551
=== mem_load_l3_hit_retired.xsnp_none ===
insert_ok : 231116
insert_alt: 175217
=== mem_load_retired.fb_hit ===
insert_ok : 6510722
insert_alt: 95952490
=== mem_load_retired.l1_hit ===
insert_ok : 698271530
insert_alt: 920982402
=== mem_load_retired.l1_miss ===
insert_ok : 69525335
insert_alt: 20089897
=== mem_load_retired.l2_hit ===
insert_ok : 1451905
insert_alt: 773356
=== mem_load_retired.l2_miss ===
insert_ok : 68085186
insert_alt: 19474303
=== mem_load_retired.l3_hit ===
insert_ok : 222829
insert_alt: 155958
=== mem_load_retired.l3_miss ===
insert_ok : 67879593
insert_alt: 19244746
=== memory_disambiguation.history_reset ===
insert_ok : 97621
insert_alt: 25831
=== minor-faults ===
insert_ok : 1048716
insert_alt: 1048718
=== node-loads ===
insert_ok : 71473780
insert_alt: 71377840
=== node-stores ===
insert_ok : 16781161
insert_alt: 16842666
=== offcore_requests.all_data_rd ===
insert_ok : 284186682
insert_alt: 392110677
=== offcore_requests.all_requests ===
insert_ok : 530876505
insert_alt: 777784382
=== offcore_requests.demand_code_rd ===
insert_ok : 34252
insert_alt: 45896
=== offcore_requests.demand_data_rd ===
insert_ok : 133468710
insert_alt: 134288893
=== offcore_requests.demand_rfo ===
insert_ok : 17612516
insert_alt: 17062276
=== offcore_requests.l3_miss_demand_data_rd ===
insert_ok : 71616594
insert_alt: 82917520
=== offcore_requests_buffer.sq_full ===
insert_ok : 2001445
insert_alt: 3113287
=== offcore_requests_outstanding.all_data_rd ===
insert_ok : 35577129549
insert_alt: 78698308135
=== offcore_requests_outstanding.cycles_with_data_rd ===
insert_ok : 17518017620
insert_alt: 7940272202
=== offcore_requests_outstanding.demand_code_rd ===
insert_ok : 11085819
insert_alt: 9390881
=== offcore_requests_outstanding.demand_data_rd ===
insert_ok : 15902243707
insert_alt: 21097348926
=== offcore_requests_outstanding.demand_data_rd_ge_6 ===
insert_ok : 1225437
insert_alt: 317436422
=== offcore_requests_outstanding.demand_rfo ===
insert_ok : 1074492442
insert_alt: 1157902315
=== offcore_response.demand_code_rd.any_response ===
insert_ok : 53675
insert_alt: 69683
=== offcore_response.demand_code_rd.l3_hit.any_snoop ===
insert_ok : 19407
insert_alt: 29704
=== offcore_response.demand_code_rd.l3_hit.snoop_none ===
insert_ok : 12675
insert_alt: 11951
=== offcore_response.demand_code_rd.l3_miss.any_snoop ===
insert_ok : 34617
insert_alt: 40868
=== offcore_response.demand_code_rd.l3_miss.spl_hit ===
insert_ok : 0
insert_alt: 753
=== offcore_response.demand_data_rd.any_response ===
insert_ok : 131014821
insert_alt: 134813171
=== offcore_response.demand_data_rd.l3_hit.any_snoop ===
insert_ok : 59713328
insert_alt: 50254543
=== offcore_response.demand_data_rd.l3_miss.any_snoop ===
insert_ok : 71431585
insert_alt: 83916030
=== offcore_response.demand_data_rd.l3_miss.spl_hit ===
insert_ok : 244837
insert_alt: 6441992
=== offcore_response.demand_rfo.any_response ===
insert_ok : 16876557
insert_alt: 17619450
=== offcore_response.demand_rfo.l3_hit.any_snoop ===
insert_ok : 907432
insert_alt: 45127
=== offcore_response.demand_rfo.l3_hit.snoop_none ===
insert_ok : 787567
insert_alt: 794579
=== offcore_response.demand_rfo.l3_hit_e.any_snoop ===
insert_ok : 496938
insert_alt: 173658
=== offcore_response.demand_rfo.l3_hit_e.snoop_none ===
insert_ok : 779919
insert_alt: 50575
=== offcore_response.demand_rfo.l3_hit_m.any_snoop ===
insert_ok : 128627
insert_alt: 25483
=== offcore_response.demand_rfo.l3_miss.any_snoop ===
insert_ok : 16782186
insert_alt: 16847970
=== offcore_response.demand_rfo.l3_miss.snoop_none ===
insert_ok : 16782647
insert_alt: 16850104
=== offcore_response.demand_rfo.l3_miss.spl_hit ===
insert_ok : 0
insert_alt: 1364
=== offcore_response.other.any_response ===
insert_ok : 137231000
insert_alt: 189526494
=== offcore_response.other.l3_hit.any_snoop ===
insert_ok : 62695084
insert_alt: 51005882
=== offcore_response.other.l3_hit.snoop_none ===
insert_ok : 62975018
insert_alt: 50217349
=== offcore_response.other.l3_hit_e.any_snoop ===
insert_ok : 62770215
insert_alt: 50691817
=== offcore_response.other.l3_hit_e.snoop_none ===
insert_ok : 62602591
insert_alt: 50642954
=== offcore_response.other.l3_miss.any_snoop ===
insert_ok : 74247236
insert_alt: 139212975
=== offcore_response.other.l3_miss.snoop_none ===
insert_ok : 75911794
insert_alt: 141076520
=== other_assists.any ===
insert_ok : 1
insert_alt: 3
=== page-faults ===
insert_ok : 1048719
insert_alt: 1048718
=== partial_rat_stalls.scoreboard ===
insert_ok : 530950991
insert_alt: 539869553
=== ref-cycles ===
insert_ok : 32546980212
insert_alt: 12930921138
=== resource_stalls.any ===
insert_ok : 21923576648
insert_alt: 5205690082
=== resource_stalls.sb ===
insert_ok : 397908667
insert_alt: 402738367
=== rs_events.empty_cycles ===
insert_ok : 1173721723
insert_alt: 1880165720
=== rs_events.empty_end ===
insert_ok : 87752182
insert_alt: 160792701
=== sw_prefetch_access.t0 ===
insert_ok : 20835202
insert_alt: 20599176
=== task-clock ===
insert_ok : 10416.86 msec task-clock:u # 1.000 CPUs utilized
insert_alt: 4767.78 msec task-clock:u # 1.000 CPUs utilized
=== tlb_flush.stlb_any ===
insert_ok : 1835393
insert_alt: 1835396
=== topdown-fetch-bubbles ===
insert_ok : 1904143421
insert_alt: 6543146396
=== topdown-slots-issued ===
insert_ok : 7538371393
insert_alt: 14449966516
=== topdown-slots-retired ===
insert_ok : 5267325162
insert_alt: 5849706597
=== uops_dispatched_port.port_0 ===
insert_ok : 1252121297
insert_alt: 1489605354
=== uops_dispatched_port.port_1 ===
insert_ok : 1379316967
insert_alt: 1585037107
=== uops_dispatched_port.port_2 ===
insert_ok : 1140861153
insert_alt: 1785053149
=== uops_dispatched_port.port_3 ===
insert_ok : 1187151423
insert_alt: 1828975838
=== uops_dispatched_port.port_4 ===
insert_ok : 1577171758
insert_alt: 1557761857
=== uops_dispatched_port.port_5 ===
insert_ok : 1341370655
insert_alt: 1653599117
=== uops_dispatched_port.port_6 ===
insert_ok : 1856735970
insert_alt: 4387464794
=== uops_dispatched_port.port_7 ===
insert_ok : 508351498
insert_alt: 603583315
=== uops_executed.core ===
insert_ok : 7225522677
insert_alt: 12716368190
=== uops_executed.core_cycles_ge_1 ===
insert_ok : 3041586797
insert_alt: 5168421550
=== uops_executed.core_cycles_ge_2 ===
insert_ok : 2017794537
insert_alt: 3653591208
=== uops_executed.core_cycles_ge_3 ===
insert_ok : 1225785335
insert_alt: 2316014066
=== uops_executed.core_cycles_ge_4 ===
insert_ok : 657121809
insert_alt: 1143390519
=== uops_executed.core_cycles_none ===
insert_ok : 22191507320
insert_alt: 6563722081
=== uops_executed.cycles_ge_1_uop_exec ===
insert_ok : 3040999757
insert_alt: 5175668459
=== uops_executed.cycles_ge_2_uops_exec ===
insert_ok : 2015520940
insert_alt: 3659989196
=== uops_executed.cycles_ge_3_uops_exec ===
insert_ok : 1224025952
insert_alt: 2319025110
=== uops_executed.cycles_ge_4_uops_exec ===
insert_ok : 657094113
insert_alt: 1141381027
=== uops_executed.stall_cycles ===
insert_ok : 22350754164
insert_alt: 6590978048
=== uops_executed.thread ===
insert_ok : 7214521925
insert_alt: 12697219901
=== uops_executed.x87 ===
insert_ok : 2992
insert_alt: 3337
=== uops_issued.any ===
insert_ok : 7531354736
insert_alt: 14462113169
=== uops_issued.slow_lea ===
insert_ok : 2136241
insert_alt: 2115308
=== uops_issued.stall_cycles ===
insert_ok : 23244177475
insert_alt: 7416801878
=== uops_retired.macro_fused ===
insert_ok : 410461916
insert_alt: 735050350
=== uops_retired.retire_slots ===
insert_ok : 5265023980
insert_alt: 5855259326
=== uops_retired.stall_cycles ===
insert_ok : 23513958928
insert_alt: 9630258867
=== uops_retired.total_cycles ===
insert_ok : 25266688635
insert_alt: 11703285605

背景

私は C++11 で暗号解読攻撃を実装しており、2 つの大きなリスト (両方ともオンザフライで生成) 間の衝突を多数検出する必要があります。したがって、攻撃の重要な部分は、2 つの重要なループで構成されています。

  1. まずハッシュテーブルに1つのリストを入力する
  2. 次に、他のリストをハッシュ テーブルと照合します。

したがって、ハッシュ テーブル操作はパフォーマンス上重要であり、速度が 3 倍低下すると、攻撃も 3 倍遅くなります。

設計について: メモリ使用量を最小限に抑えることに加え、一般的なハッシュ テーブル操作を単一のキャッシュ ラインで実行するようにしています。これにより、特にすべての CPU コアで攻撃を実行する場合に、全体的な攻撃パフォーマンスが向上すると予想しています。

ベストアンサー1

まとめ

TLDRは、TLBのすべてのレベルを逃す(したがってページウォークを必要とする)ロードと、住所不明ストアは並列実行できない、つまりロードはシリアル化され、メモリレベルの並列処理(MLP)係数は1に制限されています。実質的に、店舗はフェンス負荷も同様lfenceです。

挿入関数の遅いバージョンではこのシナリオになりますが、他の 2 つではこのシナリオになりません (ストア アドレスが既知)。領域サイズが大きい場合、メモリ アクセス パターンが支配的になり、パフォーマンスは MLP にほぼ直接関係します。高速バージョンでは、ロード ミスが重複して MLP が約 3 になり、3 倍のスピードアップが実現します (以下で説明する狭い再現ケースでは、10倍Skylake の違い)。

根本的な理由は、Skylakeプロセッサがページテーブルの一貫性これは仕様では必須ではありませんが、ソフトウェアのバグを回避することができます。

詳細

興味のある方のために、何が起こっているのか詳細に掘り下げてみます。

私は Skylake i7-6700HQ マシンですぐに問題を再現することができ、余分な部分を取り除くことで、元のハッシュ挿入ベンチマークを次の単純なループに減らすことができましたが、同じ問題が発生します。

tlb_fencing:

    xor     eax, eax  ; the index pointer
    mov     r9 , [rsi + region.start]

    mov     r8 , [rsi + region.size]  
    sub     r8 , 200                   ; pointer to end of region (plus a bit of buffer)

    mov     r10, [rsi + region.size]
    sub     r10, 1 ; mask

    mov     rsi, r9   ; region start

.top:
    mov     rcx, rax
    and     rcx, r10        ; remap the index into the region via masking
    add     rcx, r9         ; make pointer p into the region
    mov     rdx, [rcx]      ; load 8 bytes at p, always zero
    xor     rcx, rcx        ; no-op
    mov     DWORD [rsi + rdx + 160], 0 ; store zero at p + 160 
    add     rax, (64 * 67)  ; advance a prime number of cache lines slightly larger than a page

    dec     rdi
    jnz     .top

    ret

これは、 4B.sizeの最も内側のループのアクセス (ロード) とB.values[B.size] = 1アクセス (ストア)とほぼ同等です。insert_ok

ループに集中して、ストライドロードと固定ストアを実行します。次に、ロード位置をページサイズ(4 KiB)より少しだけ前方に移動します。重要なのは、ストアアドレスです。依存するロードの結果: アドレス指定式には、ロードされた値1を保持するレジスタが[rsi + rdx + 160]含まれています。ループ内でアドレス コンポーネントは変更されないため、ストアは常に同じアドレスに対して行われます (したがって、常に L1 キャッシュ ヒットが予想されます)。rdx

元のハッシュの例では、より多くの作業が行われ、メモリにランダムにアクセスし、ロードと同じ行にストアが行われていましたが、この単純なループでも同じ効果が得られます。

我々はベンチマークの別のバージョンも使用します。これは、xor rcx, rcxロードとストアの間のno-opが に置き換えられていることを除いて同一ですxor rdx, rdx休憩ロード アドレスとストア アドレス間の依存関係。

単純に考えれば、この依存関係が大きな効果をもたらすとは考えられません。ここにある店舗はファイアアンドフォーゲット:保存された場所から再度読み取ることはありません (少なくとも多くの反復では)。そのため、それらは継承される依存関係チェーンの一部ではありません。小さな領域の場合、ボトルネックは約 8 個の uop を処理することであり、大きな領域の場合、すべてのキャッシュ ミスを処理する時間が支配的になると予想されます。重要なのは、ロード アドレスは単純な非メモリ uop から独立して計算できるため、多くのミスが並列で処理されると予想されることです。

以下に、4 KiB から 256 MiB までの領域サイズについて、次の 3 つのバリエーションでサイクル単位のパフォーマンスを示します。

2M デップ:上記のループ(ストアアドレスはロードに依存)では、2 MiB の巨大ページ

4K 解像度:上記のループ (ストア アドレスはロードに依存) は標準の 4 KiB ページです。

4K独立:上記のループのバリエーションですが、ロード結果とストア アドレス間の依存関係を断ち切るために 4 KiB ページを使用してxor rdx, rdx置き換えています。xor rcx, rcx

結果:

領域が 8 MiB 以上の場合に dep ケースが吸い込まれることを示します

すべてのバリアントのパフォーマンスは、小さな領域サイズでは基本的に同じです。256 KiBまでのすべては、ループ内の8つのuopと、4 uops/サイクルのCPU幅少し計算してみると、MLP (メモリ レベルの並列処理) が適切であることがわかります。L2 キャッシュ ヒットのレイテンシは 12 サイクルですが、2 サイクルごとに 1 つ完了するため、これを達成するには平均して 6 回の L1 ミスのレイテンシを重ねる必要があります。

256 KiB から 4096 KiB の間では、L3 ヒットが発生し始めるとパフォーマンスが多少低下しますが、パフォーマンスは良好で MLP は高くなります。

8196 KiBではパフォーマンスが著しく低下し、のみ4K 解像度150サイクルを超えて最終的に約220サイクルで安定します。10回他の2つのケースよりも遅い2

すでにいくつかの重要な観察結果が示されています。

  • 両方とも200万デップそしてその4K独立ケースは速いので、これはただストア間の依存関係だけでなく、ページングの動作についても説明します。
  • 200万デップこの場合は最も高速なので、メモリが不足している場合でも依存関係によって根本的な問題が発生しないことがわかります。
  • スローのパフォーマンス4K 解像度このケースは私のマシンのメモリ遅延と疑わしいほど似ています。

上記で MLP について説明し、観測されたパフォーマンスに基づいて MLP の下限を計算しましたが、Intel CPU では 2 つのパフォーマンス カウンターを使用して MLP を直接測定できます。

l1d_pend_miss.pending

未処理の L1D ミスの期間、つまり、デマンド読み取りに必要な未処理の Fill Buffers (FB) の各サイクル数をカウントします。

l1d_pend_miss.pending_cycles

L1Dロードのサイクル未処理のミス

最初のカウンタは、L1Dからの未処理のリクエスト数を毎サイクルカウントします。つまり、3つのミスが進行中の場合、このカウンタは毎サイクル3ずつ増加します。2番目のカウンタは、少なくとも毎サイクル1ずつ増加します。1つl1d_pend_miss.pending / l1d_pend_miss.pending_cyclesミスが進行中です。これは、サイクルごとに 1 で飽和する最初のカウンターのバージョンとして見ることができます。一定期間にわたるこれらのカウンターの比率は、ミスが未解決の場合の平均 MLP 係数3です。

MLP比率をプロットしてみましょう退去そして独立した4Kベンチマークのバージョン:

4K の依存関係の場合、MLP は 8 MiB で 1 に低下することがわかります。

問題は非常に明確になります。4096 KiB の領域までは、パフォーマンスは同じで、MLP は高くなります (非常に小さな領域サイズでは、L1D ミスがまったくないため、MLP は「ありません」)。8192 KiB で突然、依存ケースの MLP は 1 に低下してそこに留まりますが、独立ケースでは MLP はほぼ 10 になります。これだけで、基本的に 10 倍のパフォーマンスの違いが説明できます。依存ケースでは、ロードをまったくオーバーラップできません。

なぜでしょうか? 問題は TLB ミスのようです。8192 KiB で何が起こるかというと、ベンチマークが TLB をミスし始めるからです。具体的には、各 Skylake コアには 1536 個の STLB (第 2 レベル TLB) エントリがあり、1536 × 4096 = 6 MiB の 4K ページをカバーできます。したがって、4 MiB と 8 MiB の領域サイズの間で、TLB ミスは に基づいて反復ごとに 1 になりdtlb_load_misses.walk_completed、このほぼ完璧すぎる偽物プロットにつながります。

両方の4kケースで8MiBで1.0ページウォークが実行されていることを示しています

つまり、次のようなことが起こります。アドレス不明のストアがストア バッファー内にある場合、STLB ミスが発生するロードは重複できません。一度に 1 つずつ実行されます。そのため、アクセスごとにメモリ全体の遅延が発生します。これは、2 MB ページのケースが高速だった理由も説明しています。2 MB ページは 3 GiB のメモリをカバーできるため、これらの領域サイズでは STLB ミス/ページ ウォークは発生しません。

なぜ

この動作は、Skylakeやその他の初期のIntelプロセッサが実装しているという事実に起因しているようです。ページテーブルの一貫性、x86 プラットフォームでは必須ではありませんが、ページ テーブルの一貫性とは、たとえば、アドレス マッピングを変更するストアの場合、再マッピングの影響を受ける仮想アドレスを使用する後続のロードでは、明示的なフラッシュなしで一貫して新しいマッピングが参照されることを意味します。

この洞察はヘンリー・ウォンの著書から得たもので、ページウォークの一貫性に関する優れた記事これを実現するために、競合または住所不明の店舗散歩中に遭遇する:

予期せぬことに、Intel Core 2 以降のシステムでは、ページ テーブルの変更がなかったにもかかわらず、ページウォークの一貫性の誤った推測が発生したかのように動作しました。これらのシステムにはメモリ依存性の予測機能があるため、ロードはストアよりもずっと前に推測的に実行され、データ依存性のチェーンが切断されるはずです。

誤って検出された誤った推測の原因は、まさに初期実行の負荷であることが判明しました。これは、一貫性違反を検出する方法についてのヒントになります。つまり、ページウォークを既知の古いストア アドレス (ストア キュー内?) と比較し、競合または不明なアドレスを持つ古いストアがある場合は一貫性違反であると想定します。

これらのストアはページテーブルを変更しないという点では全く無害ですが、ページテーブル一貫性メカニズムに巻き込まれます。この理論のさらなる証拠は、イベントを見ることで見つけることができますdtlb_load_misses.miss_causes_a_walk。イベントとは異なりwalk_completed、これはすべてのウォークをカウントします。開始正常に完了しない場合でも、次のようになります (ここでも、2M はページ ウォークをまったく開始しないため表示されません)。

dep の場合、反復ごとに 2 回以上のウォークがあることがわかります。

えっ!4K依存の番組ウォークが開始されましたが、そのうち 1 つだけが正常に完了しました。つまり、ロードごとに 2 つのウォークが行われます。これは、反復 N+1 のロードのページ ウォークが開始されるが、反復 N のストアがストア バッファーにまだ残っている (反復 N のロードがそのアドレスを提供し、まだ進行中であるため) という理論と一致します。アドレスが不明であるため、Henry が説明したようにページ ウォークはキャンセルされます。それ以降のページ ウォークは、ストア アドレスが解決されるまで延期されます。結果として、ロード N+1 のページ ウォークはロード N の結果を待機する必要があるため、すべてのロードがシリアル化されて完了します。

「悪い」方法と「代替」方法が速い理由

最後に、謎が 1 つ残っています。上記では、元のハッシュ アクセスが遅い理由は説明されていますが、他の 2 つが高速である理由は説明されていません。重要なのは、ロードによるデータ依存関係が投機的な制御依存関係に置き換えられているため、高速メソッドの両方にアドレス不明のストアがないことです。

アプローチの内部ループを見てみましょうinsert_bad

for (size_t i = 0; i < bucket_size; ++i)
{
    if (i == B.size)
    {
        B.keys[i] = k;
        B.values[i] = 1;
        ++B.size;
        ++table_count;
        return;
    }
}

ストアはループインデックスを使用することに注意してください。インデックスがストアから取得される場合とiは異なり、は単にレジスタ内の計算された値です。insert_ok[B.size]ii関連しているロードされた値にB.size最終値が等しいそれに似ていますが、これは推測された制御依存関係である比較によって確立されます。ページ ウォークのキャンセルでは問題は発生しません。このシナリオでは、ループの終了が予測不可能であるため、予測ミスが多く発生しますが、大規模な領域の場合、これらは実際にはそれほど有害ではありません。これは、通常、不良パスは正常なパスと同じメモリ アクセスを行うため (具体的には、挿入される次の値は常に同じ)、メモリ アクセスの動作が優先されるためです。

同じことがケースにも当てはまりますalt。書き込むインデックスは、計算された値を使用してi値をロードし、それが特別なマーカー値であるかどうかを確認し、インデックスを使用してその場所に書き込むことによって確立されますi。この場合も、遅延ストア アドレスはなく、すばやく計算されたレジスタ値と推測された制御依存関係のみです。

他のハードウェアについて

質問者と同様に、Skylakeで効果を確認しましたが、Haswellでも同じ動作を確認しました。Ice Lakeでは再現できません。退去そして独立したほぼ同じパフォーマンスを発揮します。

しかし、ユーザーNoahは、タイガーレイクで繁殖できると報告した特定のアラインメントに対して元のベンチマークを使用します。最も可能性の高い原因は、TGL がこのページ ウォーク動作の影響を受けないことですが、一部のアラインメントではメモリの曖昧さ回避予測子が衝突し、非常によく似た効果が発生します。つまり、プロセッサがストアがロードに転送される可能性があると判断するため、ロードは以前のアドレス不明のストアより先に実行できません。

自分で実行する

上で説明したベンチマークを自分で実行することもできます。これはuarchベンチLinux (または WSL、ただしパフォーマンス カウンターは使用できません) では、次のコマンドを実行して結果を収集できます。

for s in 2M-dep 4K-dep 4K-indep; do ./uarch-bench --timer=perf --test-name="studies/memory/tlb-fencing/*$s" --extra-events=dtlb_load_misses.miss_causes_a_walk#walk_s,dtlb_load_misses.walk_completed#walk_c,l1d_pend_miss.pending#l1d_p,l1d_pend_miss.pending_cycles#l1d_pc; done

一部のシステムでは、使用可能な空きパフォーマンス カウンターが十分にない場合があるため (ハイパースレッディングが有効になっている場合)、毎回異なるカウンター セットを使用して 2 回実行することができます。


1この場合、rdxは常にゼロ (領域全体がゼロで満たされている) なので、ストア アドレスは、このレジスタがアドレス指定式に含まれていない場合と同じになりますが、CPU はそれを認識しません。

2ここでは、200万デップケースも、4K独立ただし、その差はわずかです。

3「未処理のミスがある場合」という部分に注意してください。MLP を として計算することもできます。l1d_pend_miss.pending / cyclesこれは、未処理のミスがあるかどうかに関係なく、一定期間の平均 MLP になります。それぞれ独自の方法で役立ちますが、ミスが常に未処理であるこのようなケースでは、ほぼ同じ値が得られます。

4はい、この例と元の例には多くの違いがあります。元のループではロード場所の近くに保存されますが、元のループではロード場所の近くに保存されます。ロード場所は反復ごとに変わります。1 ではなく 0 を保存します。B.size大きすぎるかどうかはチェックしません。テストでは、ロードされた値は常に 0 です。バケットがいっぱいになったときの検索ループはありません。アドレスにランダムな値をロードするのではなく、線形ストライドを実行します。ただし、これらは重要ではありません。どちらの場合でも同じ効果が発生し、この単純なケースに到達するまで複雑さを排除して元の例を段階的に変更できます。

おすすめ記事