Tengo una aplicación donde necesito alrededor de 850 MB de memoria continua y accedo a ella de manera aleatoria. Me sugirieron asignar una página enorme de 1 GB, para que siempre estuviera en TLB. He escrito una demostración con accesos secuenciales/aleatorios para medir el rendimiento de una página pequeña (4 KB en mi caso) frente a una página grande (1 GB):
#include <inttypes.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <time.h> #include <unistd.h> #define MAP_HUGE_2MB (21 << MAP_HUGE_SHIFT) // Aren't used in this example. #define MAP_HUGE_1GB (30 << MAP_HUGE_SHIFT) #define MESSINESS_LEVEL 512 // Poisons caches if LRU policy is used. #define RUN_TESTS 25 void print_usage() { printf("Usage: ./program small|huge1gb sequential|random\n"); } int main(int argc, char *argv[]) { if (argc != 3 && argc != 4) { print_usage(); return -1; } uint64_t size = 1UL * 1024 * 1024 * 1024; // 1GB uint32_t *ptr; if (strcmp(argv[1], "small") == 0) { ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, // basically malloc(size); MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (ptr == MAP_FAILED) { perror("mmap small"); exit(1); } } else if (strcmp(argv[1], "huge1gb") == 0) { ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_1GB, -1, 0); if (ptr == MAP_FAILED) { perror("mmap huge1gb"); exit(1); } } else { print_usage(); return -1; } clock_t start_time, end_time; start_time = clock(); if (strcmp(argv[2], "sequential") == 0) { for (int iter = 0; iter < RUN_TESTS; iter++) { for (uint64_t i = 0; i < size / sizeof(*ptr); i++) ptr[i] = i * 5; } } else if (strcmp(argv[2], "random") == 0) { // pseudorandom access pattern, defeats caches. uint64_t index; for (int iter = 0; iter < RUN_TESTS; iter++) { for (uint64_t i = 0; i < size / MESSINESS_LEVEL / sizeof(*ptr); i++) { for (uint64_t j = 0; j < MESSINESS_LEVEL; j++) { index = i + j * size / MESSINESS_LEVEL / sizeof(*ptr); ptr[index] = index * 5; } } } } else { print_usage(); return -1; } end_time = clock(); long double duration = (long double)(end_time - start_time) / CLOCKS_PER_SEC; printf("Avr. Duration per test: %Lf\n", duration / RUN_TESTS); // write(1, ptr, size); // Dumps memory content (1GB to stdout). }Y en mi máquina (más abajo) los resultados son:
Secuencial:
$ ./test small sequential Avr. Duration per test: 0.562386 $ ./test huge1gb sequential <--- slightly better Avr. Duration per test: 0.543532Aleatorio:
$ ./test small random <--- better Avr. Duration per test: 2.911480 $ ./test huge1gb random Avr. Duration per test: 6.461034 Me molesta la prueba aleatoria, ¡parece que una página de 1 GB es 2 veces más lenta! Intenté usar madvise con MADV_SEQUENTIAL / MADV_SEQUENTIAL para las pruebas respectivas, pero no sirvió de nada.
¿Por qué el uso de una página enorme en caso de accesos aleatorios degrada el rendimiento? ¿Cuáles son los casos de uso de páginas grandes (2 MB y 1 GB) en general?
No probé este código con páginas de 2 MB, creo que probablemente debería funcionar mejor. También sospecho que dado que una página de 1 GB se almacena en un banco de memoria, probablemente tenga algo que ver con los canales múltiples . Pero me gustaría saber de ustedes amigos. Gracias.
Nota: para ejecutar la prueba, primero debe habilitar las páginas de 1 GB en su kernel. Puede hacerlo dándole al kernel estos parámetros hugepagesz=1G hugepages=1 default_hugepagesz=1G . Más: https://wiki.archlinux.org/index.php/Kernel_parameters . Si está habilitado, debería obtener algo como:
$ cat /proc/meminfo | grep Huge AnonHugePages: 0 kB ShmemHugePages: 0 kB FileHugePages: 0 kB HugePages_Total: 1 HugePages_Free: 1 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 1048576 kB Hugetlb: 1048576 kB EDIT1: Mi máquina tiene Core i5 8600 y 4 bancos de memoria de 4 GB cada uno. La CPU admite de forma nativa páginas de 2 MB y 1 GB (tiene pse y pdpe1gb , consulte: https://wiki.debian.org/Hugepages#x86_64 ). Estaba midiendo el tiempo de la máquina, no el tiempo de la CPU, actualicé el código y los resultados ahora son un promedio de 25 pruebas.
También me dijeron que esta prueba funciona mejor en páginas de 2 MB que en las normales de 4 KB.
No es una respuesta, sino proporcionar más detalles a este tema desconcertante.
Los contadores de rendimiento muestran una cantidad similar de instrucciones, pero aproximadamente el doble de la cantidad de ciclos utilizados cuando se utilizan páginas grandes:
Estos números de IPC dicen que el código tiene un cuello de botella en el acceso a la memoria (el IPC vinculado a la CPU en Skylake es 3 y superior). Enormes páginas cuello de botella más difícil.
Modifiqué su punto de referencia para usar MAP_POPULATE | MAP_LOCKED | MAP_FIXED con dirección fija 0x600000000000 para ambos casos para eliminar la variación de tiempo asociada con las fallas de página y la dirección de asignación aleatoria. En mi sistema Skylake, 2MiB y 1GiB son más del doble de lentos que las páginas de 4kiB.
Compilado con g++-8.4.0 -std=gnu++14 -pthread -m{arch,tune}=skylake -O3 -DNDEBUG :
[max@supernova:~/src/test] $ sudo hugeadm --pool-pages-min 2MB:64 --pool-pages-max 2MB:64 [max@supernova:~/src/test] $ sudo hugeadm --pool-pages-min 1GB:1 --pool-pages-max 1GB:1 [max@supernova:~/src/test] $ for s in small huge; do sudo chrt -f 40 taskset -c 7 perf stat -dd ./release/gcc/test $s random; done Duration: 2156150 Performance counter stats for './release/gcc/test small random': 2291.190394 task-clock (msec) # 1.000 CPUs utilized 1 context-switches # 0.000 K/sec 0 cpu-migrations # 0.000 K/sec 53 page-faults # 0.023 K/sec 11,448,252,551 cycles # 4.997 GHz (30.83%) 3,268,573,978 instructions # 0.29 insn per cycle (38.55%) 430,248,155 branches # 187.784 M/sec (38.55%) 758,917 branch-misses # 0.18% of all branches (38.55%) 224,593,751 L1-dcache-loads # 98.025 M/sec (38.55%) 561,979,341 L1-dcache-load-misses # 250.22% of all L1-dcache hits (38.44%) 271,067,656 LLC-loads # 118.309 M/sec (30.73%) 668,118 LLC-load-misses # 0.25% of all LL-cache hits (30.73%) <not supported> L1-icache-loads 220,251 L1-icache-load-misses (30.73%) 286,864,314 dTLB-loads # 125.203 M/sec (30.73%) 6,314 dTLB-load-misses # 0.00% of all dTLB cache hits (30.73%) 29 iTLB-loads # 0.013 K/sec (30.73%) 6,366 iTLB-load-misses # 21951.72% of all iTLB cache hits (30.73%) 2.291300162 seconds time elapsed Duration: 4349681 Performance counter stats for './release/gcc/test huge random': 4385.282466 task-clock (msec) # 1.000 CPUs utilized 1 context-switches # 0.000 K/sec 0 cpu-migrations # 0.000 K/sec 53 page-faults # 0.012 K/sec 21,911,541,450 cycles # 4.997 GHz (30.70%) 2,175,972,910 instructions # 0.10 insn per cycle (38.45%) 274,356,392 branches # 62.563 M/sec (38.54%) 560,941 branch-misses # 0.20% of all branches (38.63%) 7,966,853 L1-dcache-loads # 1.817 M/sec (38.70%) 292,131,592 L1-dcache-load-misses # 3666.84% of all L1-dcache hits (38.65%) 27,531 LLC-loads # 0.006 M/sec (30.81%) 12,413 LLC-load-misses # 45.09% of all LL-cache hits (30.72%) <not supported> L1-icache-loads 353,438 L1-icache-load-misses (30.65%) 7,252,590 dTLB-loads # 1.654 M/sec (30.65%) 440 dTLB-load-misses # 0.01% of all dTLB cache hits (30.65%) 274 iTLB-loads # 0.062 K/sec (30.65%) 9,577 iTLB-load-misses # 3495.26% of all iTLB cache hits (30.65%) 4.385392278 seconds time elapsed Se ejecutó en Ubuntu 18.04.5 LTS con Intel i9-9900KS (que no es NUMA), 4x8GiB 4GHz CL17 RAM en las 4 ranuras, con regulador de performance para no escalar la frecuencia de la CPU, ventiladores de refrigeración líquida al máximo para no estrangulamiento térmico, prioridad FIFO 40 sin preferencia, en un núcleo de CPU específico sin migración de CPU, ejecuciones múltiples. Los resultados son similares con el compilador clang++-8.0.0 .
Parece que hay algo sospechoso en el hardware, como un búfer de tienda por marco de página, por lo que las páginas de 4KiB permiten ~2 veces más tiendas por unidad de tiempo.
Sería interesante ver resultados para las CPU AMD Ryzen 3.
En AMD Ryzen 3 5950X, la versión de páginas grandes es solo un 10 % más lenta:
Duration: 1578723 Performance counter stats for './release/gcc/test small random': 1,726.89 msec task-clock # 1.000 CPUs utilized 0 context-switches # 0.000 K/sec 0 cpu-migrations # 0.000 K/sec 1,947 page-faults # 0.001 M/sec 8,189,576,204 cycles # 4.742 GHz (33.02%) 3,174,036 stalled-cycles-frontend # 0.04% frontend cycles idle (33.14%) 95,950 stalled-cycles-backend # 0.00% backend cycles idle (33.25%) 3,301,760,473 instructions # 0.40 insn per cycle # 0.00 stalled cycles per insn (33.37%) 480,276,481 branches # 278.116 M/sec (33.49%) 864,075 branch-misses # 0.18% of all branches (33.59%) 709,483,403 L1-dcache-loads # 410.844 M/sec (33.59%) 1,608,181,551 L1-dcache-load-misses # 226.67% of all L1-dcache accesses (33.59%) <not supported> LLC-loads <not supported> LLC-load-misses 78,963,441 L1-icache-loads # 45.726 M/sec (33.59%) 46,639 L1-icache-load-misses # 0.06% of all L1-icache accesses (33.51%) 301,463,437 dTLB-loads # 174.570 M/sec (33.39%) 301,698,272 dTLB-load-misses # 100.08% of all dTLB cache accesses (33.28%) 54 iTLB-loads # 0.031 K/sec (33.16%) 2,774 iTLB-load-misses # 5137.04% of all iTLB cache accesses (33.05%) 243,732,886 L1-dcache-prefetches # 141.140 M/sec (33.01%) <not supported> L1-dcache-prefetch-misses 1.727052901 seconds time elapsed 1.579089000 seconds user 0.147914000 seconds sys Duration: 1628512 Performance counter stats for './release/gcc/test huge random': 1,680.06 msec task-clock # 1.000 CPUs utilized 1 context-switches # 0.001 K/sec 1 cpu-migrations # 0.001 K/sec 1,947 page-faults # 0.001 M/sec 8,037,708,678 cycles # 4.784 GHz (33.34%) 4,684,831 stalled-cycles-frontend # 0.06% frontend cycles idle (33.34%) 2,445,415 stalled-cycles-backend # 0.03% backend cycles idle (33.34%) 2,217,699,442 instructions # 0.28 insn per cycle # 0.00 stalled cycles per insn (33.34%) 281,522,918 branches # 167.567 M/sec (33.34%) 549,427 branch-misses # 0.20% of all branches (33.33%) 312,930,677 L1-dcache-loads # 186.261 M/sec (33.33%) 1,614,505,314 L1-dcache-load-misses # 515.93% of all L1-dcache accesses (33.33%) <not supported> LLC-loads <not supported> LLC-load-misses 888,872 L1-icache-loads # 0.529 M/sec (33.33%) 13,140 L1-icache-load-misses # 1.48% of all L1-icache accesses (33.33%) 9,168 dTLB-loads # 0.005 M/sec (33.33%) 870 dTLB-load-misses # 9.49% of all dTLB cache accesses (33.33%) 1,173 iTLB-loads # 0.698 K/sec (33.33%) 1,914 iTLB-load-misses # 163.17% of all iTLB cache accesses (33.33%) 253,307,275 L1-dcache-prefetches # 150.772 M/sec (33.33%) <not supported> L1-dcache-prefetch-misses 1.680230802 seconds time elapsed 1.628170000 seconds user 0.052005000 seconds sysIntel tuvo la amabilidad de responder a este problema. Vea su respuesta a continuación.
Este problema se debe a cómo se comprometen realmente las páginas físicas. En caso de páginas de 1GB, la memoria es contigua. Entonces, tan pronto como escriba en cualquier byte dentro de la página de 1 GB, se asigna la página completa de 1 GB. Sin embargo, con las páginas de 4 KB, las páginas físicas se asignan cuando toca por primera vez en cada una de las páginas de 4 KB.
for (uint64_t i = 0; i < size / MESSINESS_LEVEL / sizeof(*ptr); i++) { for (uint64_t j = 0; j < MESSINESS_LEVEL; j++) { index = i + j * size / MESSINESS_LEVEL / sizeof(*ptr); ptr[index] = index * 5; } }En el bucle más interno, el índice cambia a un ritmo de 512 KB. Entonces, las referencias consecutivas se asignan a compensaciones de 512 KB. Por lo general, los cachés tienen 2048 conjuntos (que son 2 ^ 11). Entonces, los bits 6:16 seleccionan los conjuntos. Pero si avanza con compensaciones de 512 KB, los bits 6:16 serían los mismos y terminarían seleccionando el mismo conjunto y perdiendo la localidad espacial.
Recomendamos inicializar todo el búfer de 1 GB secuencialmente (en la prueba de página pequeña) como se muestra a continuación antes de iniciar el reloj para cronometrarlo.
for (uint64_t i = 0; i < size / sizeof(*ptr); i++) ptr[i] = i * 5;Básicamente, el problema es con los conflictos de conjuntos que resultan en fallas de caché en el caso de páginas grandes en comparación con páginas pequeñas debido a desplazamientos constantes muy grandes. Cuando usa compensaciones constantes, la prueba realmente no es aleatoria .