En bref :
J'ai implémenté une simple table de hachage (à clés multiples) avec des buckets (contenant plusieurs éléments) qui correspondent exactement à une cacheline. L'insertion dans un seau de cacheline est très simple, et la partie critique de la boucle principale.
J'ai mis en place trois versions qui produisent le même résultat et devraient se comporter de la même manière.
Le mystère
Cependant, je constate des différences de performance importantes, d'un facteur 3, alors que toutes les versions ont exactement le même modèle d'accès à la ligne de cache et produisent des données de table de hachage identiques.
La meilleure mise en œuvre insert_ok
subit un ralentissement d'environ un facteur 3 par rapport aux insert_bad
& insert_alt
sur mon CPU (i7-7700HQ). Une variante insert_bad est une simple modification de insert_ok
qui ajoute une recherche linéaire supplémentaire inutile dans la ligne de cache pour trouver la position où écrire (qu'il connaît déjà) et ne subit pas ce ralentissement de x3.
Le même exécutable montre insert_ok
un facteur 1,6 plus rapide par rapport à insert_bad
& insert_alt
sur d'autres CPU (AMD 5950X (Zen 3), Intel i7-11800H (Tiger Lake)).
# 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)
Le code
// 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;
}
}
Mon analyse
J'ai essayé diverses modifications de la boucle C++, mais elle est intrinsèquement si simple que le compilateur produira le même assemblage. Il n'est pas vraiment évident, à partir de l'assemblage résultant, de savoir ce que la perte du facteur 3 pourrait causer. J'ai essayé de mesurer la performance, mais je n'arrive pas à trouver de différence significative.
En comparant l'assemblage des 3 versions qui ne sont que des boucles relativement petites, il n'y a rien qui suggère quelque chose de proche qui pourrait causer une perte de facteur 3 entre ces versions.
Par conséquent, je suppose que le ralentissement de 3x est un effet bizarre de la préextraction automatique, ou de la prédiction de branchement, ou de l'alignement des instructions/sauts ou peut-être une combinaison de ces éléments.
Quelqu'un a-t-il une meilleure idée ou de meilleurs moyens de mesurer les effets qui pourraient être en jeu ici ?
Détails
J'ai créé un petit exemple fonctionnel en C++11 qui démontre le problème. Le code est disponible à l'adresse https://github.com/cr-marcstevens/hashtable_mystery
Cela inclut également mes propres binaires statiques qui démontrent ce problème sur mon CPU, car des compilateurs différents peuvent produire un code différent. Ainsi que du code assembleur vidéo pour les trois versions de table de hachage.
mesures d'événements perf
Voici un grand nombre de mesures d'événements de perforation. Je me suis concentré sur celles qui incluent le mot miss
y stall
. Chaque événement comporte deux lignes :
- la première ligne correspond à
insert_ok
qui a le ralentissement -
la deuxième ligne correspond à
insert_alt
qui comporte une boucle supplémentaire et un travail supplémentaire, mais qui est finalement plus rapide=== 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
Contexte
J'implémente une attaque cryptographique en C++11 et je dois trouver de nombreuses collisions entre deux grandes listes (toutes deux générées à la volée). Une partie cruciale de l'attaque consiste donc juste en deux boucles critiques :
- en remplissant d'abord une table de hachage avec une liste
- puis en comparant l'autre liste avec la table de hachage.
Les opérations de la table de hachage sont donc critiques en termes de performances, et un ralentissement de facteur 3 signifie que l'attaque est 3x plus lente.
En ce qui concerne la conception : En plus d'essayer de minimiser l'utilisation de la mémoire, j'essaie également de faire en sorte qu'une opération typique de table de hachage ne fonctionne que sur une seule ligne de cache. Je m'attends à ce que cela augmente les performances globales de l'attaque, en particulier lorsque l'attaque est exécutée sur tous les cœurs du processeur.
2 votes
Pourriez-vous ajouter l'objdump de chaque version sur le github ? Je pense à deux possibilités : 1)
b.size
se déverse. 2)b.size
est régulièrement 0 ou 1 et très prévisible, donc les versions qui bouclent suri
permettent essentiellement de "sauter" la dépendance mémoire de l'index. Par ailleurs, quels compteurs de performance changent de valeur entre les versions ? Je vérifierais au moins si c'est lié à FE aveclsd.uops
,idq_dsb.uops
etidq_mite.uops
. Vous pouvez également vérifier la distribution des uops et des misses de branche sur le port.3 votes
J'ai fait quelques recherches. J'ai pu le reproduire sur Tigerlake. Notez que
insert_ok
a un mode "lent" et "rapide". Le seul compteur de perf que je vois qui semble vraiment prédire les résultats est le suivantmachine_clears_memory_ordering
. Contrairement à ce qui était prévu, avec un compteur clair élevé, on obtient un mode "rapide" et avec un compteur clair bas, on obtient un mode "lent". Il est possible que le faible nombre d'effacements de la machine indique que le ralentissement provient de l'entrée dans un état sérialisé en raison des facteurs suivants désambiguïsation de la mémoire .0 votes
@Noah l'objdump est en fait déjà dans le repo, il suffit de vérifier le sous-répertoire data. Si par déversement de b.size vous voulez dire à travers la limite de la ligne de cache, alors cela ne se produit pas et est vérifié par static_asserts. Je vais essayer de comprendre les compteurs de performance et je vous le ferai savoir, je ne suis pas vraiment familier avec eux.
2 votes
@PeterCordes pourrait en savoir plus sur le sens de cette démarche. (Notez également que si j'ajoute quelque chose comme
size_t sz = 0; if(sz != B.size) { sz = B.size; }....
et utilisersz
pour l'indexer, il s'agit exclusivement de la version la plus rapide. Cela indique que le problème de performance est lié à une certaine sérialisation / dépendance entre la charge deB.size
et le calcul de l'adresse de stockage.1 votes
Un autre élément intéressant est que dans le mode "lent", nous constatons une augmentation (environ 33%) de
port78
(ports de magasin sur Tigerlake). @PeterCordes sera également mieux informé, mais est-il possible que le magasin dépendant de la charge exécute "préemptivement" le hit L1 attendu sur la charge et que nous obtenions des replays ? Je ne sais pas comment l'expliquer autrement.0 votes
@Noah cela semble très intéressant et vous suggérez déjà une meilleure solution potentielle que je vais essayer. Pourriez-vous expliquer comment vous pouvez voir qu'il y a un mode rapide et un mode lent de insert_ok ? En tout cas, pour moi, le programme est constamment lent.
1 votes
@MarcStevens Je pense que c'est lié à un certain seuil de taille. J'ai diminué la taille des tests pour qu'ils s'exécutent plus rapidement (2^23) et j'ai vu environ 4/5 "mode rapide" où
insert_ok
était à peu près aussi rapide que les autres et 1/5 en "mode lent" où il était un ordre de grandeur plus lent.2 votes
Je note que
insert_ok
est exécuté en premier dans tous vos tests. Pour éliminer les phénomènes tels que l'échauffement du cache, l'étranglement du CPU, etc., obtenez-vous le même résultat si vous exécutez vos trois fonctions dans un ordre différent ?0 votes
@Noah (je publierai bientôt les données de perforation). J'ai essayé le
size_t sz = 0; if (sz != B.size) { sz = B.size; }
et en utilisantsz
comme indice. Cela n'a pas accéléré les choses, à moins que je n'enlève l'élément++B.size;
. Cependant, en supprimant cette ligne, vous obtiendrez toujours le cas trivial où B.size == 0, et ce ne sera plus correct.1 votes
@NateEldredge Oui, en fait j'avais l'habitude de les exécuter dans un ordre différent. Cela n'a pas d'importance. De plus, nous parlons de tables de hachage de 1 Go, ce qui est considérablement plus grand que les caches. Chaque fonction utilise une table différente pour pouvoir vérifier que les tables à la fin sont identiques, ce qui implique qu'il n'y a pas non plus d'effet de cache entre les fonctions.
0 votes
@Noah : Je me demande si ce n'est pas l'erratum du JCC qui le rend lent sur Skylake. ( La routine alignée sur 32 octets ne s'adapte pas au cache uops ) Je ne sais pas si cela peut expliquer un ralentissement de 3x, cependant. Est-ce que ICL / TGL corrige cet erratum, ne pas trébucher sur les branches à la fin d'un bloc de 32 octets ? Je n'ai pas vérifié le désassemblage (et la question ne montre pas la version du compilateur) donc je ne suis pas sûr d'être capable de le reproduire.
0 votes
@PeterCordes Beaucoup d'informations supplémentaires, y compris la version du compilateur et le désassemblage réel des trois fonctions sont dans le repo github lié dans le sous-répertoire data.
0 votes
Ok, je n'ai pas remarqué de branchements conditionnels à la fin d'un bloc de 32 octets en github.com/cr-marcstevens/hashtable_mystery/blob/main/data/ donc ça n'a pas l'air d'être ça.
2 votes
@PeterCordes je ne pense pas que ce soit lié à FE. Les tests effectués sur ma machine n'indiquent rien (et j'ai supprimé l'inlining et aligné toutes les fonctions sur 256 octets). Je pense vraiment que c'est lié à un blocage introduit par les magasins/charges qui ne peuvent pas être réorganisés.
0 votes
@Noah : Oh, je vois votre commentaire précédent maintenant que vous pourriez reprocher sur Tiger Lake. Mais la question dit
insert_ok
fonctionne 1,6 fois plus vite queinsert_bad
sur leur CPU Tiger Lake. On peut donc supposer qu'il s'agit de binaires différents sur ces machines, au moins à cause de-march=native
et peut-être des noyaux ou des bibliothèques différents alignant les données différemment dans la mémoire ?1 votes
@PeterCordes mais si je peux reproduire les résultats tout en contrôlant l'alignement, je pense que cela indique que un peu de La différence de perf est probablement sans rapport avec la FE. D'autant plus que je ne voyais aucun résultat de compteur de perforation sur les compteurs liés à FE qui indiquait la déviation.
1 votes
La grande différence d'ordre dans l'effacement des machines, les versions "rapides" ayant un nombre élevé d'effacements et les versions "lentes" un nombre faible d'effacements, me fait penser que la différence est en quelque sorte liée à la sérialisation causée par la désambiguïsation de la mémoire. Une réflexion sur la
insert_ok
est que c'est la seule où il y a une dépendance non spéculative entre la charge deB.size
et l'adresse du magasin.0 votes
@PeterCordes Maintenant que vous le dites. J'ai testé exactement le même exécutable avec l'assemblage repo sur l'AMD 5950X et j'ai vu insert_ok environ 1,6x plus rapide que les autres versions. Cependant, la TigerLake a été testée par quelqu'un d'autre et a également rapporté 1,6x plus rapide. Bien qu'il puisse s'agir d'un assemblage différent, ils l'ont téléchargé sur leur propre clone repo : github.com/kientuong114/hashtable_mystery
1 votes
Pouvez-vous tester votre système TGL avec le même binaire qui était lent sur votre Kaby Lake ?
3 votes
Je ne sais pas si cela peut aider mais je peux reproduire ce problème sur un i7-870 (Nehalem) aussi. Pas de reproduction sur mon i9-10900 (Comet Lake) ni sur mon i7-6550U (Skylake) par contre.
2 votes
Avez-vous utilisé le même binaire sur tous ? C'est très sensible au compilateur : de petits changements peuvent faire passer les versions rapides à la vitesse lente.
2 votes
@TravisDown a pu fournir de bonnes idées : twitter.com/trav_downs/status/1451400596238397444 Cela a conduit à au moins trois pistes potentielles pour contourner le problème :
0 votes
(1) faire du bitbanging pour mettre à jour la ligne de cache et avoir des magasins non dépendants de la taille B. Je ne l'ai pas encore testé.
0 votes
(2) pour mon cas d'utilisation : insertions par lot et d'abord préfixer les cachelines du lot entier et ensuite traiter. J'ai testé et cela réduit insert_ok de ~11400ms à ~1500ms : un facteur 7.6 plus rapide ! twitter.com/realhashbreaker/status/1451448137860751360
0 votes
(3) peut-être quelque chose de plus générique avec le même effet que (2) : avoir une file d'attente d'insertion de taille fixe dans la table de hachage. Chaque insertion prélivre la ligne de cache pour elle-même et entre dans la file d'attente, mais traite le premier élément de la file. Pour maintenir l'exactitude de la table de hachage, toute recherche devrait d'abord déclencher l'effacement de la file d'attente. Je n'ai pas testé cela. (Je suppose que les types de clés et de valeurs sont petits et triviaux).
0 votes
L'explication de Travis semble assez bonne, vous pouvez la publier ainsi que votre solution en tant que réponse personnelle si vous le souhaitez.
0 votes
@MarcoBonelli : Travis est ici @BeeOnRope. Il pourrait éventuellement le poster lui-même, puisqu'il a déjà commenté cette question-réponse.
0 votes
@Peter ah c'est logique, je me demandais s'il avait un compte ou pas.
0 votes
@Noah - oui, vous aviez raison sur votre supposition de MLP faible en raison de la sérialisation des charges. Il est intéressant que vous l'ayez trouvé aussi sur TGL, je n'ai pas pu le reproduire sur ICL, et je ne me serais pas attendu à ce que TGL soit différent à cet égard.
1 votes
@BeeOnRope Il s'avère donc que pour reproduire sur ICL/TGL, j'ai dû changer l'alignement des fonctions des wrappers d'insertion. J'avais réaligné la fonction
hash_table_*
fonctions wrapper à 256 octets pour supprimer le bruit FE. Je peux "reproduire" avec un alignement de 256 octets. I ne peut pas reproduire avec un alignement de 64 octets. Je pense que cela indique l'unité de désambiguïsation de la mémoire, car c'est la seule façon dont je peux voir cet alignement affecter la MLP. Notez qu'avec un alignement 64/256, je ne vois aucune différence dans les compteurs liés à la FE.