Загрузка CPU в 100% не означает, что процессор всё время выполняет полезные инструкции: он может просто ждать данные из памяти. Современный CPU способен обрабатывать порядка 4–8 инструкций за наносекунду, тогда как обращение к DRAM занимает примерно 50–100 наносекунд. За это время процессор мог бы выполнить сотни операций. Кэши L1–L3 сокращают задержку, но их объём ограничен, поэтому размер рабочего набора данных критичен для производительности.
Автор демонстрирует проблему на GROUP BY в собственном движке Pivot. Наивная реализация хранит одну растущую хеш-таблицу и обновляет счётчик для каждого ключа. Запрос к набору ClickBench обрабатывает 99 миллионов строк и около 17 миллионов уникальных UserID. При таком количестве групп таблица не помещается в кэш, и почти каждое обращение идёт в оперативную память.
Тест на одном ядре AMD EPYC 9124 занимал 3,6 секунды при повторном запуске. Профилирование показало: 50,1% процессорных слотов простаивали из-за памяти, лишь 16,1% завершали микрооперации. За запрос из DRAM по требованию загружалось около 4,5 ГБ данных.
Затем команда добавила radix partitioning. На первом этапе строки распределяются по 4096 разделам по старшим 12 битам хеша ключа. На втором для каждого раздела по очереди создаётся небольшая хеш-таблица и выполняется агрегация. Операций становится больше, зато рабочие таблицы чаще помещаются в кэш.
Повторный запуск ускорился до 2,7 секунды. Ожидание памяти снизилось до 23,5%, доля завершаемых операций выросла до 32,2%, а чтение из DRAM уменьшилось до 1,5 ГБ. При этом число выполненных операций увеличилось примерно с 11,4 до 19,4 миллиарда. Полная реализация Pivot показала 1,848 секунды и 954 МБ чтений из DRAM. Практический вывод: дополнительные вычисления выгодны, если они заменяют дорогие холодные обращения к памяти.