Ich bin am Whiskey Lake i7-8565U und analysiere die Leistungsindikatoren und die Zeit für das Kopieren von 512 KiB Daten (doppelt so viel wie die L2-Cache-Größe) und habe einige Missverständnisse in Bezug auf die Arbeit des L2 HW-Prefetchers.
Im Intel Manual Vol.4 MSR gibt es MSR, 0x1A4
dessen Bit 0 zur Steuerung des L2 HW-Prefetcher dient (1 zum Deaktivieren).
Betrachten Sie den folgenden Benchmark:
memcopy.h
::
void *avx_memcpy_forward_lsls(void *restrict, const void *restrict, size_t);
memcopy.S
::
avx_memcpy_forward_lsls:
shr rdx, 0x3
xor rcx, rcx
avx_memcpy_forward_loop_lsls:
vmovdqa ymm0, [rsi + 8*rcx]
vmovdqa [rdi + rcx*8], ymm0
vmovdqa ymm1, [rsi + 8*rcx + 0x20]
vmovdqa [rdi + rcx*8 + 0x20], ymm1
add rcx, 0x08
cmp rdx, rcx
ja avx_memcpy_forward_loop_lsls
ret
main.c
::
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include <x86intrin.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include "memcopy.h"
#define ITERATIONS 1000
#define BUF_SIZE 512 * 1024
_Alignas(64) char src[BUF_SIZE];
_Alignas(64) char dest[BUF_SIZE];
static void __run_benchmark(unsigned runs, unsigned run_iterations,
void *(*fn)(void *, const void*, size_t), void *dest, const void* src, size_t sz);
#define run_benchmark(runs, run_iterations, fn, dest, src, sz) \
do{\
printf("Benchmarking " #fn "\n");\
__run_benchmark(runs, run_iterations, fn, dest, src, sz);\
}while(0)
int main(void){
int fd = open("/dev/urandom", O_RDONLY);
read(fd, src, sizeof src);
run_benchmark(20, ITERATIONS, avx_memcpy_forward_lsls, dest, src, BUF_SIZE);
}
static inline void benchmark_copy_function(unsigned iterations, void *(*fn)(void *, const void *, size_t),
void *restrict dest, const void *restrict src, size_t sz){
while(iterations --> 0){
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
fn(dest, src, sz);
}
}
static void __run_benchmark(unsigned runs, unsigned run_iterations,
void *(*fn)(void *, const void*, size_t), void *dest, const void* src, size_t sz){
unsigned current_run = 1;
while(current_run <= runs){
benchmark_copy_function(run_iterations, fn, dest, src, sz);
printf("Run %d finished\n", current_run);
current_run++;
}
}
Betrachten Sie 2 Läufe des kompilierten main.c
Ich .
MSR:
$ sudo rdmsr -p 0 0x1A4
0
Run:
$ taskset -c 0 sudo ../profile.sh ./bin
Performance counter stats for './bin':
10 486 164 071 L1-dcache-loads (12,13%)
10 461 354 384 L1-dcache-load-misses # 99,76% of all L1-dcache hits (12,05%)
10 481 930 413 L1-dcache-stores (12,05%)
10 461 136 686 l1d.replacement (12,12%)
31 466 394 422 l1d_pend_miss.fb_full (12,11%)
211 853 643 294 l1d_pend_miss.pending (12,09%)
1 759 204 317 LLC-loads (12,16%)
31 007 LLC-load-misses # 0,00% of all LL-cache hits (12,16%)
3 154 901 630 LLC-stores (6,19%)
15 867 315 545 l2_rqsts.all_pf (9,22%)
0 sw_prefetch_access.t1_t2 (12,22%)
1 393 306 l2_lines_out.useless_hwpf (12,16%)
3 549 170 919 l2_rqsts.pf_hit (12,09%)
12 356 247 643 l2_rqsts.pf_miss (12,06%)
0 load_hit_pre.sw_pf (12,09%)
3 159 712 695 l2_rqsts.rfo_hit (12,06%)
1 207 642 335 l2_rqsts.rfo_miss (12,02%)
4 366 526 618 l2_rqsts.all_rfo (12,06%)
5 240 013 774 offcore_requests.all_data_rd (12,06%)
19 936 657 118 offcore_requests.all_requests (12,09%)
1 761 660 763 offcore_response.demand_data_rd.any_response (12,12%)
287 044 397 bus-cycles (12,15%)
36 816 767 779 resource_stalls.any (12,15%)
36 553 997 653 resource_stalls.sb (12,15%)
38 035 066 210 uops_retired.stall_cycles (12,12%)
24 766 225 119 uops_executed.stall_cycles (12,09%)
40 478 455 041 uops_issued.stall_cycles (12,05%)
24 497 256 548 cycle_activity.stalls_l1d_miss (12,02%)
12 611 038 018 cycle_activity.stalls_l2_miss (12,09%)
10 228 869 cycle_activity.stalls_l3_miss (12,12%)
24 707 614 483 cycle_activity.stalls_mem_any (12,22%)
24 776 110 104 cycle_activity.stalls_total (12,22%)
48 914 478 241 cycles (12,19%)
12,155774555 seconds time elapsed
11,984577000 seconds user
0,015984000 seconds sys
II.
MSR:
$ sudo rdmsr -p 0 0x1A4
1
Run:
$ taskset -c 0 sudo ../profile.sh ./bin
Performance counter stats for './bin':
10 508 027 832 L1-dcache-loads (12,05%)
10 463 643 206 L1-dcache-load-misses # 99,58% of all L1-dcache hits (12,09%)
10 481 296 605 L1-dcache-stores (12,12%)
10 444 854 468 l1d.replacement (12,15%)
29 287 445 744 l1d_pend_miss.fb_full (12,17%)
205 569 630 707 l1d_pend_miss.pending (12,17%)
5 103 444 329 LLC-loads (12,17%)
33 406 LLC-load-misses # 0,00% of all LL-cache hits (12,17%)
9 567 917 742 LLC-stores (6,08%)
1 157 237 980 l2_rqsts.all_pf (9,12%)
0 sw_prefetch_access.t1_t2 (12,17%)
301 471 l2_lines_out.useless_hwpf (12,17%)
218 528 985 l2_rqsts.pf_hit (12,17%)
938 735 722 l2_rqsts.pf_miss (12,17%)
0 load_hit_pre.sw_pf (12,17%)
4 096 281 l2_rqsts.rfo_hit (12,17%)
4 972 640 931 l2_rqsts.rfo_miss (12,17%)
4 976 006 805 l2_rqsts.all_rfo (12,17%)
5 175 544 191 offcore_requests.all_data_rd (12,17%)
15 772 124 082 offcore_requests.all_requests (12,17%)
5 120 635 892 offcore_response.demand_data_rd.any_response (12,17%)
292 980 395 bus-cycles (12,17%)
37 592 020 151 resource_stalls.any (12,14%)
37 317 091 982 resource_stalls.sb (12,11%)
38 121 826 730 uops_retired.stall_cycles (12,08%)
25 430 699 605 uops_executed.stall_cycles (12,04%)
41 416 190 037 uops_issued.stall_cycles (12,04%)
25 326 579 070 cycle_activity.stalls_l1d_miss (12,04%)
25 019 148 253 cycle_activity.stalls_l2_miss (12,03%)
7 384 770 cycle_activity.stalls_l3_miss (12,03%)
25 442 709 033 cycle_activity.stalls_mem_any (12,03%)
25 406 897 956 cycle_activity.stalls_total (12,03%)
49 877 044 086 cycles (12,03%)
12,231406658 seconds time elapsed
12,226386000 seconds user
0,004000000 seconds sys
Ich bemerkte den Schalter:
12 611 038 018 cycle_activity.stalls_l2_miss
v / s
25 019 148 253 cycle_activity.stalls_l2_miss
Dies deutet darauf hin, dass der MSR-Deaktivierungs-L2-HW-Prefetcher angewendet wird. Auch andere l2 / LLC-bezogene Dinge unterscheiden sich erheblich. Der Unterschied ist über verschiedene Läufe reproduzierbar . Das Problem ist, dass es fast keinen Unterschied in total time
und Zyklen gibt:
48 914 478 241 cycles
v / s
49 877 044 086 cycles
12,155774555 seconds time elapsed
v / s
12,231406658 seconds time elapsed
FRAGE: Werden
L2-Fehler von anderen Leistungsbegrenzern verdeckt?
Wenn ja, können Sie vorschlagen, welche Zähler zu betrachten sind, um sie zu verstehen?
Antworten:
Ja, der L2-Streamer ist die meiste Zeit sehr hilfreich.
memcpy hat keine Rechenlatenz zum Ausblenden, daher kann es sich wohl leisten, OoO Exec-Ressourcen (ROB-Größe) die zusätzliche Lastlatenz zu überlassen, die Sie durch mehr L2-Fehler erhalten, zumindest in diesem Fall, wenn Sie alle L3-Treffer erhalten Bei Verwendung eines mittelgroßen Arbeitssatzes (1 MB), der in L3 passt, ist kein Vorabruf erforderlich, um L3-Treffer zu erzielen.
Die einzigen Anweisungen sind Laden / Speichern (und Schleifen-Overhead), sodass das OoO-Fenster Bedarfslasten für ziemlich weit voraus enthält.
IDK, wenn der räumliche L2-Prefetcher und der L1d-Prefetcher hier helfen.
Vorhersage zum Testen dieser Hypothese : Vergrößern Sie Ihr Array, damit Sie L3-Fehler erhalten, und Sie werden wahrscheinlich einen Unterschied in der Gesamtzeit feststellen, wenn OoO exec nicht ausreicht, um die Ladelatenz bis zum DRAM zu verbergen. HW-Prefetch-Triggerung weiter vorne kann einigen helfen.
Die anderen großen Vorteile des HW-Prefetching liegen darin, dass es mit Ihrer Berechnung Schritt halten kann , sodass Sie L2-Treffer erhalten. (In einer Schleife, die mit einer mittellangen, aber nicht schleifengetragenen Abhängigkeitskette berechnet wurde.)
Demand Loads und OoO Exec können viel dazu beitragen, die verfügbare Speicherbandbreite (Single Threaded) zu nutzen, wenn kein anderer Druck auf die ROB-Kapazität ausgeübt wird.
Beachten Sie außerdem, dass auf Intel-CPUs jeder Cache- Fehler eine Back-End-Wiedergabe (vom RS / Scheduler) abhängiger Uops kosten kann , jeweils eine für L1d- und L2- Fehler , wenn die Daten voraussichtlich eintreffen. Und danach spammt der Kern anscheinend optimistisch, während er darauf wartet, dass Daten von L3 eintreffen.
(Siehe https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th und Are load ops freigegeben von der RS wenn sie versenden, vervollständigen oder zu einem anderen Zeitpunkt? )
Nicht das Cache-Miss-Laden selbst; In diesem Fall wäre es die Geschäftsanweisung. Genauer gesagt, die Speicherdaten für Port 4. Das spielt hier keine Rolle. Die Verwendung von 32-Byte-Speichern und Engpässen bei der L3-Bandbreite bedeutet, dass wir nicht in der Nähe von 1 Port 4 UOP pro Takt sind.
quelle
16MiB
Puffer und10
Iterationen durchgeführt und tatsächlich14,186868883 seconds
vs43,731360909 seconds
und46,76% of all LL-cache hits
vs erhalten99,32% of all LL-cache hits
.1 028 664 372 LLC-loads
vs1 587 454 298 LLC-loads
.Ja, der L2 HW Prefetcher ist sehr hilfreich!
Die folgenden Ergebnisse finden Sie beispielsweise auf meinem Computer (i7-6700HQ), auf dem tinymembench ausgeführt wird . In der ersten Ergebnisspalte sind alle Prefetchers aktiviert, in der zweiten Ergebnisspalte ist der L2-Streamer ausgeschaltet (alle anderen Prefetchers sind jedoch noch aktiviert).
Bei diesem Test werden 32-MiB-Quell- und Zielpuffer verwendet, die viel größer als der L3 auf meinem Computer sind. Daher werden hauptsächlich DRAM-Fehler getestet.
Bei diesen Tests ist der L2-Streamer nie langsamer und oft fast doppelt so schnell.
Im Allgemeinen stellen Sie möglicherweise die folgenden Muster in den Ergebnissen fest:
standard memset
undSTOSB fill
(diese laufen auf dieser Plattform auf dasselbe hinaus) sind am wenigsten betroffen, wobei das vorabgerufene Ergebnis nur wenige% schneller ist als ohne.memcpy
ist hier wahrscheinlich die einzige Kopie, die 32-Byte-AVX-Anweisungen verwendet, und sie gehört zu den am wenigsten betroffenen Kopien - aber das Vorabrufen ist immer noch ~ 40% schneller als ohne.Ich habe auch versucht, die anderen drei Prefetchers ein- und auszuschalten, aber sie hatten im Allgemeinen fast keinen messbaren Effekt für diesen Benchmark.
quelle
vmovdqa
Unterhaltsame Tatsache: Ist AVX1 trotz "Ganzzahl".) Glauben Sie, dass die OP-Schleife eine geringere Bandbreite als glibc memcpy bietet? Und deshalb waren 12 LFBs genug, um mit der Nachfrage nach L3 Schritt zu halten, ohne den zusätzlichen MLP aus der L2 <-> L3-Superqueue zu nutzen, den der L2-Streamer belegen kann? Das ist vermutlich der Unterschied in Ihrem Test. L3 sollte mit der gleichen Geschwindigkeit wie der Kern laufen. Sie haben beide Quad-Core-Skylake-Client-äquivalente Mikroarchitekturen, also wahrscheinlich eine ähnliche L3-Latenz?uarch-bench
in den Kommentaren gesehen).