В чем разница между уровнями кэширования процессора
Что проверяют
Для Go-разработчика вопрос не про железо ради железа. Интервьюер хочет понять, объясните ли вы, почему слайс структур обходится быстрее слайса указателей и откуда берётся false sharing. Следом часто просят показать, как это проявляется в коде.
Как отвечать
Начните с иерархии и порядков величин, оговорившись, что точные цифры зависят от процессора:
- L1: у каждого ядра свой, разделён на кэш инструкций и кэш данных, десятки килобайт, доступ около 4-5 тактов;
- L2: обычно тоже на ядро, от сотен килобайт до нескольких мегабайт, порядка 12-16 тактов;
- L3: общий для всех ядер или кластера, единицы и десятки мегабайт, несколько десятков тактов;
- оперативная память: 60-100 нс, это сотни тактов.
Обмен идёт не байтами, а кэш-линиями, на x86 это 64 байта. Когда ядро пишет в линию, протокол когерентности (MESI и его варианты) инвалидирует её копии в кэшах других ядер.
Выводы для Go:
- локальность данных:
[]Pointлежит в памяти подряд и читается линиями, а[]*Pointзаставляет прыгать по куче и заодно нагружает GC; - порядок обхода: двумерный массив обходите по строкам, обход по столбцам даёт промах почти на каждом шаге;
- false sharing: два счётчика, которые разные горутины обновляют через
sync/atomic, но которые лежат в одной линии, будут постоянно гонять её между ядрами. Лечится выравниванием, напримерcpu.CacheLinePadизgolang.org/x/sys/cpuмежду полями.
Гипотезы проверяйте бенчмарком go test -bench и счётчиками perf stat -e cache-misses, а не на глаз.
Где спотыкаются
Называют задержку L1 в 1-2 такта, а L3 в 4-10. Это цифры из старых шпаргалок, на деле L3 стоит десятки тактов.
Добавляют паддинг везде «для скорости». Выравнивание раздувает структуры и ухудшает локальность, оправдано оно только для горячих полей, которые пишут разные ядра, и только после замера.
Путают кэш процессора с кэшированием в приложении и начинают рассказывать про Redis и sync.Map. Уточните, о каком кэше речь, и отвечайте про железо.