为什么我的Intel Skylake / Kaby Lake CPU在一个简单的哈希表实现中会遇到神秘的3倍减速因素?

79

简而言之:

我实现了一个简单的(多键)哈希表,其中的桶(包含多个元素)完全适合缓存行。将元素插入到缓存行桶中非常简单,也是主循环的关键部分。

我已经实现了三个版本,它们产生相同的结果,并且应该具有相同的行为。

谜团

但是,尽管所有版本都具有相同的缓存行访问模式,并导致相同的哈希表数据,但我看到了惊人的大因子3的性能差异。

最佳实现insert_ok在我的CPU(i7-7700HQ)上比insert_badinsert_alt慢了约3倍。

一种名为insert_bad的变体是对insert_ok的简单修改,它在缓存行内添加了不必要的线性搜索以找到要写入的位置(已知),并且不会出现这种x3的性能下降。

在其他CPU(AMD 5950X(Zen 3),Intel i7-11800H(Tiger Lake))上,完全相同的可执行文件显示insert_okinsert_badinsert_alt快了1.6倍。

# 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倍减速是自动预取、分支预测、指令/跳转对齐或者可能是这些因素的组合产生的奇怪效果。

是否有人有更好的洞察力或测量方式,可以确定此处实际发挥作用的影响?

详细信息

我创建了一个小的工作C++11示例,演示了这个问题。 该代码可在https://github.com/cr-marcstevens/hashtable_mystery上获得。

这也包括我的自己的静态二进制文件,它演示了这个问题在我的CPU上,因为不同的编译器可能会产生不同的代码。以及所有三个哈希表版本的转储汇编代码。

perf事件测量

这里有很多perf事件测量结果。我关注包含missstall字样的事件。 每个事件有两行:

  • 第一行对应insert_ok,它减速了
  • 第二行对应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实现一个密码分析攻击,需要在两个大列表之间找到许多碰撞(两个列表都是动态生成的)。 因此,攻击的关键部分只包括两个关键循环:

  1. 首先用一个列表填充哈希表
  2. 然后将另一个列表与哈希表进行匹配。

因此,哈希表操作对性能至关重要。如果操作变慢了3倍,那么攻击的速度就会变慢3倍。

关于设计: 除了尝试最小化内存使用外,我还试图使一个典型的哈希表操作仅操作单个缓存行。我期望这将增加整体攻击性能,特别是在所有CPU核心上运行攻击时。


2
你能否将每个版本的objdump添加到github中?我有两种可能性:1)b.size正在溢出。2)b.size通常为0或1,并且高度可预测,因此在循环i的版本实际上“跳过”了索引的内存依赖关系。此外,哪些性能计数器在版本之间更改值?我至少会检查是否与lsd.uopsidq_dsb.uopsidq_mite.uops相关的FE问题。您还可以检查uops和分支未命中的端口分配。 - Noah
3
进行了一些调查,成功重现了在Tigerlake上的问题。请注意insert_ok具有“慢”和“快”两种模式。我能看到唯一真正能够预测结果的性能计数器是machine_clears_memory_ordering。与预期相反,当清除计数高时,我们获得“快速”模式;而当清除计数低时,我们获得“慢速”模式。这可能表明,由于内存消歧而进入序列化状态导致了减速,因为机器清除较低。 - Noah
2
@PeterCordes 可能更了解这是否有意义。 (还要注意,如果我添加类似size_t sz = 0; if(sz!= B.size){sz = B.size;} ....并使用sz进行索引,则是最快的版本。这确实指出性能问题与某些序列化/依赖项之间的负载和存储地址计算有关。 - Noah
3
@Peter,不确定这会不会有帮助,但我也可以在 i7-870(Nehalem)上重现此问题。但在我的i9-10900(Comet Lake)和i7-6550U(Skylake)上无法重现。 - Marco Bonelli
2
@TravisDown 能够提供很好的见解:https://twitter.com/trav_downs/status/1451400596238397444 这至少引出了三种可能绕过问题的途径: - Marc Stevens
显示剩余25条评论
1个回答

89

总结

简而言之,所有未命中TLB的负载(因此需要进行页面查找),并且被“地址未知”的存储器所隔开的负载无法并行执行,即负载被串行化,内存级别的并行性(MLP)因子被限制在1。实际上,存储器会“栅栏”(fence)加载,就像lfence一样。

您的插入函数的慢版本会导致这种情况,而其他两个版本则不会(存储器地址已知)。对于大区域大小,内存访问模式占主导地位,性能几乎直接与MLP相关:快速版本可以重叠负载未命中,并获得约3倍的MLP,从而实现3倍的加速(我们将在下面讨论更窄的复制案例,在Skylake上显示超过10倍的差异)。

根本原因似乎是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

这大致相当于insert_ok最内层循环的B.size访问(加载)和B.values[B.size] = 1访问(存储)。专注于循环,我们进行跨步加载和固定存储。然后将加载位置向前移动一点超过页面大小(4 KiB)。关键是,存储地址取决于加载结果:因为 addressing 表达式[rsi + rdx + 160]包括rdx,它是保存的值得寄存器1。存储始终发生在同一地址上,因为循环中没有任何地址组成部分发生变化(因此我们预计总是会有L1缓存命中)。
原始哈希示例做了更多的工作,并随机访问了存储器,并且将存储置于与加载相同的行中,但此简单循环捕获了相同的效果。
我们还使用了基准测试的另一个版本,除了在加载和存储之间的无操作xor rcx,rcx被替换为xor rdx,rdx。这破坏了加载和存储地址之间的依赖性。
从表面上看,我们不指望这种依赖性会做太多事情。这里的存储是点燃并忘记:我们不再从存储位置读取(至少在许多迭代之后不再读取),因此它们不是任何承载依赖链的一部分。对于小区域,我们预计瓶颈将仅通过 ~8 uops 进行处理,并且对于大区域,我们预计处理所有缓存未命中的时间将占主导地位:关键是,我们预计许多未命中将同时处理,因为加载地址可以从简单的非内存 uops 中独立计算出来。
以下是区域大小从4 KiB到256 MiB的性能表现,其中包含以下三个变化: 2M dep: 显示上述循环的循环(存储地址依赖于加载),使用2 MiB huge pages4K dep: 显示上述循环的循环(存储地址依赖于加载),使用标准 4 KiB 页面。 4K indep: 上述循环的变体,但使用xor rdx,rdx替换了xor rcx,rcx以破坏加载结果和存储地址之间的依赖关系,使用4KiB页面。
结果:

Shows the dep case sucking when region is 8 MiB or more

所有变体的性能在小区域大小上基本相同。直到256 KiB,每次迭代需要2个周期,仅受循环中的8个uops和CPU宽度为4个uops /周期的限制。一些数学计算表明我们有不错的MLP(内存级并行性):L2缓存命中的延迟为12个周期,但我们每2个周期完成一个,因此平均而言,我们必须重叠6个L1缺失的延迟才能实现这一点。
在256 KiB和4096 KiB之间,性能会有所下降,因为开始出现L3命中,但性能良好且MLP高。
在8196 KiB时,仅对于 4K dep 情况,性能会灾难性地下降,超过150个周期,并最终稳定在约220个周期。 它比其他两种情况2慢了10倍以上。
我们已经可以得出一些关键观察结果:
  • 2M dep 4K indep 两种情况都很快:因此这不仅涉及存储之间的依赖关系,还涉及分页行为。
  • 2M dep 情况是最快的,因此我们知道即使在错失内存时,依赖关系也不会导致某些根本性问题。
  • 慢的 4K dep 情况的性能与我的机器的内存延迟非常相似。
我已经在上面提到了MLP并计算了基于观察性能的MLP下限,但在Intel CPU上,我们可以使用两个性能计数器直接测量MLP:

l1d_pend_miss.pending

计算L1D未命中的持续时间,即由Demand Reads需要的每个周期的Fill Buffers(FB)数量。

l1d_pend_miss.pending_cycles

具有L1D加载未命中的周期

第一个计数器每个周期计算从L1D发出多少个请求。因此,如果有3个未命中正在进行中,则该计数器每个周期增加3。第二个计数器每个周期增加1,至少有一个未命中正在进行中。您可以将其视为每个周期饱和为1的第一个计数器版本。这些计数器的比率 l1d_pend_miss.pending / l1d_pend_miss.pending_cycles 是任何未命中未完成时的平均MLP因子3
让我们为4K基准测试的 dep indep 版本绘制MLP比率:

Shows that MLP tanks in the 4K dep case to 1 at 8 MiB

问题变得非常明显。在4096 KiB的区域内,性能相同,并且MLP很高(对于非常小的区域大小,由于根本没有L1D缺失,因此“没有”MLP)。突然在8192 KiB处,依赖情况下的MLP下降到1并保持不变,而独立情况下的MLP接近10。这基本上解释了10倍的性能差异:依赖情况下无法重叠负载。

为什么?问题似乎是TLB缺失。在8192 KiB发生了什么是基准测试开始错过TLB。具体来说,每个Skylake核心有1536个STLB(第二级TLB)条目,可以覆盖1536×4096 = 6 MiB的4K页面。因此,在4和8 MiB的区域大小之间,基于dtlb_load_misses.walk_completed,TLB缺失每次迭代增加1,导致出现了这个几乎完美到不真实的图:

Shows that 1.0 page walks are done for both 4k cases at 8 MiB

这就是发生的事情:当存储缓冲区中有地址未知的存储时,需要进行STLB(快表)缺失的加载无法重叠:它们一个接一个地执行。因此,每次访问都要承受完整的内存延迟。这也解释了为什么2MB页面的情况很快:2 MB页面可以覆盖3 GiB的内存,因此对于这些区域大小,没有STLB缺失/页面遍历。
为什么?
这种行为似乎源于Skylake和其他早期的Intel处理器实现页面表一致性,尽管x86平台不需要它。页面表一致性意味着如果修改地址映射的存储(例如),使用受重新映射影响的虚拟地址的后续加载将始终看到新映射而无需进行任何显式刷新。
这个想法来自Henry Wong,在他的excellent article on page walk coherence中报告说,为了做到这一点,在页面遍历过程中,如果遇到冲突或地址未知的存储,则会终止页面遍历:
“出乎意料的是,即使没有进行页面表修改,Intel Core 2和更高版本的系统的行为也像是发生了页面遍历一致性错误。这些系统具有内存依赖性预测,因此加载应该比存储更早地进行推测执行,并且打破数据依赖性链。”
“事实证明,正是早期执行的加载导致了错误检测到的misspeculation。这提供了一些关于如何检测一致性违规的提示:通过将页面遍历与已知的旧存储地址(在存储队列中?)进行比较,并假定如果存在具有冲突或未知地址的旧存储,则存在一致性违规。”
因此,即使这些存储是完全无辜的,因为它们不修改任何页面表,它们也被卷入了页面表一致性机制中。我们可以通过查看事件dtlb_load_misses.miss_causes_a_walk来找到进一步证据。与walk_completed事件不同,这个事件计算所有已经开始的遍历,即使它们没有成功完成。它看起来像这样(再次,2M没有显示,因为它根本不会启动页面遍历):

Shows that the dep case has slightly more than 2 walks per iteration

哎呀!4K依赖性显示了两个开始的步骤,其中只有一个成功完成。每次加载都需要两次步骤。这符合理论,即页面步骤从迭代N+1中的负载开始,但它发现迭代N中的存储仍然在存储缓冲区中(因为迭代N的负载提供其地址,并且仍在进行中)。由于地址未知,Henry所描述的页面步骤被取消。进一步的页面步骤被延迟,直到解析存储地址。结果是所有负载都以序列化方式完成,因为负载N+1的页面步骤必须等待负载N的结果。

为什么“bad”和“alt”方法很快

最后,还有一个谜团。上面解释了原始哈希访问为什么慢,但没有解释其他两个方法为什么快。关键在于两种快速方法都没有地址未知的存储,因为通过将数据依赖性替换为推测控制依赖性,与负载的数据依赖性得到了替换。

看一下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]来自存储,而i只是一个在寄存器中计算出的值。现在i与加载的值B.size相关,因为它的最终值将等于它,但这是通过比较建立的,这是一种推测的控制依赖关系。它不会导致页面遍历取消任何问题。这种情况确实有很多误判(因为循环退出是不可预测的),但对于大区域的情况,这些并不是太有害,因为坏路径通常会进行与好路径相同的内存访问(具体来说,下一个插入的值始终相同),而内存访问行为占主导地位。

alt情况也是如此:要写入的索引是通过使用计算出的值i来加载值,检查它是否是特殊标记值,然后使用索引i在该位置写入的。同样,没有延迟存储地址,只有快速计算的寄存器值和推测的控制依赖性。

其他硬件怎么样

像问题作者一样,我发现Skylake上的效果,但我也观察到在Haswell上出现了相同的行为。在Ice Lake上,我无法重现它:dep和indep的性能几乎相同。

然而,用户Noah 报告说他可以在Tigerlake上复制使用原始基准测试某些对齐方式。我认为最有可能的原因是TGL没有受到这种页面遍历行为的影响,而是在某些对齐方式下,内存消歧预测器会发生冲突,从而导致非常类似的效果:加载无法在较早的地址未知存储之前执行,因为处理器认为存储可能会转发到负载。

自己运行

您可以自己运行我上面描述的基准测试。它是uarch-bench的一部分。在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

如果启用了超线程,某些系统可能没有足够的可用性能计数器,因此您可以每次使用不同的计数器集进行两次运行。


1在这种情况下,rdx始终为零(该区域完全由零填充),因此存储地址恰好与如果不包括该寄存器在寻址表达式中相同,但CPU并不知道!

2在这里,2M dep的情况也开始显示出比4K indep更好的性能,尽管差距很小。

3请注意“当有任何未命中时”部分:您还可以将MLP计算为l1d_pend_miss.pending / cycles,这将是一个时间段内的平均MLP,而不管是否有任何未命中。它们各自有用,但在像这样持续存在未命中的情况下,它们给出几乎相同的值。

4是的,这个和原始示例之间有很多不同之处。我们存储到单个固定位置,而原始循环存储在负载位置附近,每次迭代都会变化。我们存储0而不是1。我们不检查B.size是否太大。在我们的测试中,加载的值始终为0。当桶已满时,没有搜索循环。我们不将随机值加载到地址中,而只是进行线性步幅。但是,这些并不重要:在两种情况下都会产生相同的效果,并且您可以通过逐渐减少复杂性来修改原始示例,直到达到这个简单的情况。


1
@Noah - 啊,谢谢你提供有关TGL的信息。正如你之前链接的我的文章所描述的那样,“内存消歧”是使得加载操作在先前地址未知的存储操作之前执行的机制。因此,如果这个机制失败了,它将产生完全相同类型的行为:加载N+1必须等待存储N完成,而存储N无法完成直到加载N完成,依此类推。 - BeeOnRope
1
如果在预测器表中有两个存储器“别名”,其中一个存储器无法始终提升(即,它与先前的某个存储器存在别名),而另一个存储器始终可以提升,则消除歧义的主要方法将失败。在这种情况下,后者的加载将无法可靠地提升(如果存在AU存储器),因为预测器会混合两个加载的状态。 - BeeOnRope
1
我很早就排除了这个问题,因为我将其归约为仅有1个负载和1个存储的情况,这样就不可能出现预测器冲突,甚至没有“第二个负载”来发生冲突。当然,在更大的示例中,你肯定可以得到这样的冲突。根据我的早期研究,预测器是以简单的方式进行索引的:256个条目由负载指令的第一个字节进行索引。因此,如果它们随机对齐,则任何两个负载都有1/256的碰撞几率,或者如果您在代码中强制使用某种类型的函数对齐(例如256字节!),则几率更高。 - BeeOnRope
1
Henry Wong使用的基准似乎将存储器称为“存储器”,用于页面表映射...整个过程涉及尝试检测更新映射的存储器,而这些只是随机存储器...但请参见“错误推测检测机制?”部分(我应该直接链接它)。在那里,他描述了页面表修改检测似乎是如何工作的。特别注意这一部分(我在回答中引用了更多上下文):并假设如果存在具有冲突或未知地址的旧存储器,则存在一致性违规。 - BeeOnRope
2
因此,机制无法确定存储是否实际上针对页面表,因为存储具有未知地址。因此,为保守起见,它取消了页面遍历。 - BeeOnRope
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接