Допустим, у нас есть указатель на счетчик (
uint32_t), значение которого нужно увеличить:uint32_t inc(uint32_t *p) {
return *p + 1;
}То же самое на ассемблере:
inc:
mov eax, dword ptr [rdi]
inc eax
ret
1.
mov загружает в регистр eax 4 байта, при промахе в L1 целая кеш-линия (64 байта) подтягивается со следующих уровней;2.
inc увеличивает значение в eax на 1.Если после этого программа завершается, то с точки зрения производительности, нас интересует только скорость доставки данных (считаем что инкремент выполняется за константное время, в 1 цикл).
Типичные тайминги:
Типичные тайминги:
| Level | Cycles | Latency (~3 GHz )
|------------------|--------|------------------
| L1D | ~4-5 | ~1.5 ns
| L2 | ~12-15 | ~4-5 ns
| L3 (LLC, local) | ~40-70 | ~14-23 ns
| DRAM (local NUMA)| — | ~80-120 ns
| DRAM (remote) | — | ~140-200 ns
Но если впереди у программы есть другая работа, картина усложняется - появляется фактор пропускной способности. Даже в рамках одного ядра (читаем про out-of-order execution, ссылки в конце).
———
Максимальную пропускную способность для одного ядра можно грубо подсчитать по формуле:
Bandwidth = N_max × line_size / miss_latencyГде:
-
N_max: число одновременных загрузок из памяти при L1 miss (memory-level parallelism), в Intel ограничено компонентом Line Fill Buffer (LFB), ~12-16 единиц;-
line_size: размер кеш-линии, 64 байта;-
miss_latency: задержка доступа к данным, пусть в 100ns. Получается, что одно ядро может потреблять порядка 10 GB/s пропускной способности памяти, при том что один канал DDR5 выдает ~38 GB/s.
И раз переутилизация каналов DRAM часто не наш кейс, все что остается это сокращать задержку доступа к данным (Latency) и/или увеличивать эффективное число загрузок
N (Throughput). Из вариантов:
* сокращать working set (рабочий набор), чтобы больше данных помещались в быстрые кеши процессора
* избегать pointer chasing, особенно в list-подобных структурах, которые ограничивают
N* продумывать layout данных: группировать поля структур по паттернам доступа, чтобы они подтягивались в рамках одной кеш-линии
* использовать software prefetch, Huge Pages и прочие техники.
———
Ссылки по теме:
- Taras Tsugrii о memory-bound
- A whirlwind introduction to dataflow graphs (pointer chasing)
- Про Line Fill Buffer (LFB)
- Out-of-order execution
- Performance Analysis and Tuning on Modern CPUs