Optimizing ClickHouse for Intel's 280 core processors
Оптимизация ClickHouse для процессоров Intel с ультра-высоким числом ядер
Авторы: Цзебин Сань, Чжиго Чжоу, Ваньян Го, Тьянью Ли
Гостевой пост от инженеров по оптимизации производительности из Intel Shanghai.
Современные процессоры Intel достигают беспрецедентного числа ядер: до 128 P-ядер на сокет в Granite Rapids и 288 E-ядер в Sierra Forest. Многосокетные системы могут иметь более 400 ядер. Тенденция «больше ядер, а не выше тактовая частота» обусловлена физическими ограничениями и проблемами энергопотребления.
Для аналитических баз данных, таких как ClickHouse, большое количество ядер представляет как возможность, так и сложную задачу. Хотя теоретически больше ядер означают больше параллельной мощности, большинство СУБД не могут полностью использовать аппаратные ресурсы. Проблемы параллельной обработки, такие как конфликты блокировок, когерентность кэша, неоднородный доступ к памяти (NUMA), пропускная способность памяти и накладные расходы на координацию, усугубляются с ростом числа ядер.
Оптимизация для ультра-высокого числа ядер
За последние три года мы анализировали и оптимизировали масштабируемость ClickHouse на процессорах Intel Xeon с большим числом ядер. С помощью инструментов профилирования (perf, emon, Intel VTune) мы исследовали все 43 запроса ClickBench на серверах с ультра-высоким числом ядер, выявляли узкие места и оптимизировали ClickHouse.
Результаты впечатляют: отдельные оптимизации ускоряют выполнение запросов в несколько раз, в некоторых случаях до 10x. Среднее геометрическое время выполнения всех запросов стабильно улучшалось на 2–10% для каждой оптимизации. Это демонстрирует, что ClickHouse можно эффективно масштабировать на системах с ультра-высоким числом ядер.
Ключевые проблемы масштабирования
Помимо производительности одного ядра, необходимо решить несколько ключевых задач:
- Накладные расходы когерентности кэша: Перемещение строк кэша между ядрами требует циклов CPU.
- Конфликты блокировок: Даже небольшие последовательные участки кода (1%) сильно ограничивают параллелизм по закону Амдала.
- Пропускная способность памяти: Эффективное использование памяти критично для систем с интенсивной работой с данными.
- Координация потоков: Стоимость синхронизации потоков растет сверхлинейно с их количеством.
- Эффекты NUMA: Задержки и пропускная способность памяти различаются для локальной и удаленной памяти в многосокетных системах.
В этом посте summarized наши оптимизации ClickHouse для серверов с ультра-высоким числом ядер.
Комментарии (46)
- Исправление опечатки в заголовке: речь идёт о 288-ядерных серверных процессорах Intel Sierra Forest, а не о "Intel 280".
- Оптимизация ClickHouse под высокоядорные системы: улучшение аллокаторов памяти, борьба с конкуренцией, использование SIMD-фильтрации (ускорение запросов до 35%).
- Обсуждение технических деталей процессоров: отсутствие AVX-512 на E-ядрах Sierra Forest, наличие AVX2 VNNI, сравнение с AMD Zen 4c.
- Проблемы масштабирования: сложности NUMA, contention points на уровне аллокаторов, разделение полосы пропускания памяти.
- Практический опыт использования ClickHouse: высокая эффективность сжатия и скорость агрегации больших данных (миллиарды снэпшотов).
- Критика и предложения: переход с jemalloc на современные аллокаторы (TCMalloc, mimalloc), использование оптимистичного контроля параллелизма.
- Исторический и футуристический контекст: сравнение с устаревшими системами, шутки о запуске 1000 виртуальных машин и размещении целого стойка в одном процессоре.
Faster substring search with SIMD in Zig
SIMD-поиск подстроки в Zig
Автор реализовал алгоритм, который на 60 % быстрее std.mem.indexOf.
Идея: сравниваем 32-байтовые блоки текста с первым и последним символом искомого слова, используя AVX2.
- Берём первый и последний байт
needle(first,last). - Загружаем 32 байта
haystackв векторBlock = @Vector(32, u8). - Создаём маски совпадений:
const eq_first = first == block; const eq_last = last == block_shifted; const mask = @bitCast(eq_first & eq_last); - Для каждого установленного бита проверяем полное совпадение подстроки.
- Хвост обрабатываем обычным
indexOf.
Код (сокращённо):
const Block = @Vector(32, u8);
const first = @splat(needle[0]);
const last = @splat(needle[k-1]);
while (i + k + 32 <= n) : (i += 32) {
const f = haystack[i..][0..32].*;
const l = haystack[i+k-1..][0..32].*;
var mask: u32 = @bitCast((first == f) & (last == l));
while (mask != 0) {
const bit = @ctz(mask);
if (mem.eql(u8, haystack[i+bit+1..][0..k-1], needle[1..]))
return i + bit;
mask &= mask - 1;
}
}
return mem.indexOf(u8, haystack[i..], needle) orelse null;
Тест: поиск слова «newsletter» во всём «Моби Дике» (~1.2 МБ).
Сборка: zig build -Doptimize=ReleaseFast.
Комментарии (52)
- Подход с SIMD-ускорением поиска подстроки популярен, но в худшем случае остаётся квадратичным
O(m·n), поэтому нужен «откат» на линейный алгоритм (KMP/BM). - Участники отмечают, что большинство реализаций опираются на 10-летние AVX/NEON, игнорируя AVX-512, SVE и RVV, которые дают больший выигрыш, но пока редки на десктопах и в облаках.
- Zig пока не предоставляет прямых intrinsics, хотя LLVM-бекенд позволяет вызывать нужные инструкции; это тормозит портирование низкоуровневых оптимизаций.
- Есть идея дальнейшего SIMD-фильтра: проверять не только первый/последний байт иглы, но и второй/предпоследний и т.д., накладывая маски.
- Вопросы Unicode: алгоритм работает на байтах, поэтому для UTF-8/16 потребуется дополнительная обработка переменной длины кодов.