Learning Platform
Глоссарий Troubleshooting
Урок 06.02 · 22 мин
Средний
vectorized-enginecpu-cachevector-size

STANDARD_VECTOR_SIZE = 2048: почему столько

В предыдущем уроке мы установили: DuckDB обрабатывает данные порциями-векторами, и это компромисс между построчной обработкой и полной материализацией. Но любой компромисс требует конкретного числа. Насколько большой должна быть порция? Десять значений? Миллион? DuckDB отвечает: 2048. Это значение константы STANDARD_VECTOR_SIZE — размера вектора по умолчанию.

2048 — не случайное и не «круглое для красоты» число. Это инженерный выбор, зажатый между двумя противоположными требованиями, и понять его — значит понять, как векторизованный движок взаимодействует с физическим устройством процессора: его кэшами и его SIMD-инструкциями. Этот урок — про обоснование числа 2048 «до железа».


Два требования тянут в разные стороны

Размер вектора определяется конфликтом двух сил.

Сила первая — «вектор должен быть большим». Из прошлого урока: накладные расходы (вызов функции, интерпретация выражения) делятся на число значений в векторе. Чем больше вектор, тем меньше накладных расходов на одно значение. Вектор из 10 значений амортизирует вызов всего в 10 раз — мало. Вектор из 2048 амортизирует его в 2048 раз — накладные расходы становятся пренебрежимыми. Эта сила хочет, чтобы вектор был как можно больше.

Сила вторая — «вектор должен быть маленьким». Промежуточные результаты — это векторы, и они должны помещаться в кэш процессора. Кэш — это маленькая сверхбыстрая память между процессором и оперативной памятью. Если вектор и промежутки влезают в кэш, процессор работает на полной скорости. Если не влезают — каждое обращение к данным идёт в медленную оперативную память, и движок упирается в неё. Эта сила хочет, чтобы вектор был как можно меньше — иначе он вытесняется из кэша.

Размер вектора зажат между двумя требованиями
Больше -> меньше накладных расходовЧем больше вектор, тем сильнее амортизируются вызовы функций и интерпретация. Эта сила тянет размер вверх.
компромисс
2048STANDARD_VECTOR_SIZE = 2048 — точка равновесия двух сил: достаточно велик для амортизации, достаточно мал, чтобы рабочие данные жили в кэше.
компромисс
Меньше -> помещается в кэшЧем меньше вектор, тем легче ему и промежуткам жить в кэше процессора. Эта сила тянет размер вниз.

2048 — точка равновесия. Достаточно большой, чтобы накладные расходы исчезли. Достаточно маленький, чтобы рабочий набор векторов жил в кэше. Чтобы увидеть, почему равновесие именно здесь, надо посмотреть на кэши процессора в цифрах.


Иерархия кэшей процессора

Процессор не обращается к оперативной памяти напрямую при каждом действии — это было бы слишком медленно. Между ядром и RAM лежит иерархия кэшей, каждый уровень больше и медленнее предыдущего:

УровеньТипичный размерСкорость доступа
Регистрыдесятки значениймгновенно
L1-кэшпримерно 32-48 КБ на ядронесколько тактов
L2-кэшпримерно 256 КБ - 2 МБ на ядрооколо десятка тактов
L3-кэшнесколько-десятки МБ, общийдесятки тактов
Оперативная память (RAM)гигабайтысотни тактов

Ключевые цифры — порядки величин разрыва. Доступ к L1 — несколько тактов. Доступ к RAM — сотни тактов. Разница примерно в сто раз. Если данные, с которыми сейчас работает оператор, лежат в L1/L2 — процессор почти не простаивает. Если они вытеснены в RAM — процессор большую часть времени ждёт данные, а не считает.

Цель размера вектора — чтобы рабочий набор оператора жил в L1/L2-кэше. Посчитаем. Вектор из 2048 значений типа INTEGER (4 байта) — это 2048 умножить на 4, то есть 8 килобайт. Вектор BIGINT (8 байт) — 16 килобайт. Оператору обычно нужно несколько векторов сразу: входной, выходной, может быть промежуточный. Несколько векторов по 8-16 КБ — это десятки килобайт. Это укладывается в L1 (32-48 КБ) или с запасом в L2. Рабочий набор векторного оператора помещается в быстрый кэш — и процессор не простаивает.

Вектор 2048 значений помещается в кэш
2048 x INTEGER (4 байта) = 8 КБОдин вектор целочисленных значений занимает 8 килобайт. Несколько таких векторов — рабочий набор оператора.
несколько векторов оператора
десятки КБ рабочего набораВходной, выходной, промежуточный векторы вместе — десятки килобайт.
влезает в L1/L2
L1 ~32-48 КБ, L2 ~256 КБ+Рабочий набор помещается в L1 или L2 — самую быструю память. Процессор не ждёт данные из RAM.

Если бы вектор был, скажем, миллион значений, один такой вектор занял бы мегабайты — он не поместился бы ни в L1, ни в L2, и движок при каждой операции упирался бы в RAM. Это была бы фактически полная материализация со всеми её бедами. Если бы вектор был 16 значений — он легко влез бы в кэш, но накладные расходы амортизировались бы лишь в 16 раз, и движок захлебнулся бы в вызовах. 2048 — посередине: рабочий набор в кэше, накладные расходы амортизированы.


Связь с SIMD

У числа 2048 есть второе обоснование — SIMD. SIMD (Single Instruction, Multiple Data) — это режим процессора, в котором одна инструкция обрабатывает сразу несколько значений: например, одна инструкция складывает не два числа, а восемь пар чисел за один такт. Современные процессоры имеют SIMD-регистры, вмещающие 4, 8, 16 значений в зависимости от ширины регистра и типа данных.

SIMD требует, чтобы данные лежали плотным массивом одного типа — ровно так, как лежит вектор DuckDB. Плотный цикл по вектору из 2048 одинаковых значений компилятор может развернуть в SIMD-инструкции: вместо 2048 отдельных сложений — 2048, делённое на ширину SIMD-регистра, векторных сложений. Построчная обработка такого не позволяет: там значения перемежаются ветвлениями и вызовами.

Почему здесь важно именно 2048, а не произвольное число? 2048 — это степень двойки (2 в 11-й степени). Степень двойки идеально делится на любую ширину SIMD-регистра (которые тоже степени двойки: 4, 8, 16). Цикл по 2048 значениям раскладывается на целое число SIMD-итераций без остатка, без «хвоста» из необработанных значений, который пришлось бы обрабатывать отдельным скалярным кодом. Кратность степени двойки делает векторный цикл аккуратным для SIMD и для компилятора.

NOTE

Степень двойки удобна не только для SIMD. Размеры структур, выравнивание данных в памяти, индексная арифметика — всё это работает аккуратнее, когда размер вектора степень двойки. 2048 — это инженерно «правильное» число: оно и попадает в нужный диапазон между кэшем и амортизацией, и при этом является степенью двойки, что упрощает всё, что связано с памятью и SIMD.


STANDARD_VECTOR_SIZE — константа компиляции

Важная деталь: STANDARD_VECTOR_SIZE — это не настройка, которую крутят в рантайме через SET. Это константа времени компиляции DuckDB. Значение 2048 зашито в собранный бинарник, и весь движок — все операторы, все структуры данных — оптимизирован именно под него.

Технически DuckDB можно собрать из исходников с другим STANDARD_VECTOR_SIZE. Но это делается крайне редко — разве что для экспериментов или для экзотических платформ. Для обычного пользователя 2048 — это данность, фиксированная характеристика движка. Знать число важно не чтобы его менять, а чтобы понимать поведение DuckDB: почему row group — это 122 880 строк (60 умножить на 2048, тема модуля про storage), почему планы оперируют тем, что они оперируют, как оценивать объёмы данных в движке.

WARNING

Не пытайтесь «настроить» размер вектора через SET или PRAGMA — такой настройки нет, потому что 2048 это константа компиляции, а не рантайм-параметр. Если вы видите совет «увеличьте размер вектора для скорости» — это недоразумение. Производительность DuckDB настраивается другими рычагами (число потоков, лимит памяти), а размер вектора уже выбран оптимально под устройство процессоров и менять его не нужно.

Итог урока: 2048 — это инженерный компромисс, зажатый между тремя физическими фактами процессора. Достаточно велик, чтобы амортизировать накладные расходы на вызовы и интерпретацию. Достаточно мал, чтобы рабочий набор векторов помещался в L1/L2-кэш, а не вытеснялся в медленную RAM. И является степенью двойки, чтобы аккуратно ложиться на SIMD-инструкции и на работу с памятью. Это число — точка, где векторизованная модель максимально эффективно использует реальное железо.


Попробуй сам

Это урок про числа — задания на расчёт и проверку.

  1. Посчитайте размер вектора в байтах для разных типов: вектор из 2048 значений TINYINT (1 байт), INTEGER (4 байта), BIGINT (8 байт), DOUBLE (8 байт). Сравните результаты с типичным размером L1-кэша (32-48 КБ).
  2. Узнайте размер кэшей вашего процессора (на macOS — sysctl -a | grep cachesize, на Linux — lscpu). Прикиньте, сколько векторов INTEGER помещается в ваш L1 и L2.
  3. Объясните своими словами, что было бы плохо, если бы STANDARD_VECTOR_SIZE равнялся 64. А если бы он равнялся 1 миллиону?
  4. 2048 — это 2 в 11-й степени. Возьмите ширину SIMD-регистра в 8 значений и убедитесь, что 2048 делится на 8 нацело. Объясните, почему отсутствие остатка важно для векторного цикла.
  5. Попробуйте найти в DuckDB настройку для размера вектора через SELECT * FROM duckdb_settings(). Объясните, почему её там нет.
DataFusion: размер батча и кэш-эффективность
Проверка знанийKnowledge check
Почему STANDARD_VECTOR_SIZE в DuckDB равен именно 2048, и какими тремя свойствами процессора обоснован этот выбор?
ОтветAnswer
STANDARD_VECTOR_SIZE = 2048 — это инженерный компромисс, зажатый между тремя физическими фактами устройства процессора. Первое требование тянет размер вверх: накладные расходы (вызов функции, интерпретация выражения) делятся на число значений в векторе, поэтому вектор должен быть большим, чтобы амортизировать их — 2048 делает накладные расходы на одно значение пренебрежимыми. Второе требование тянет размер вниз: промежуточные результаты-векторы должны помещаться в кэш процессора. Между ядром и RAM лежит иерархия кэшей (L1 примерно 32-48 КБ, L2 примерно 256 КБ и больше), и доступ к L1 в сотню раз быстрее доступа к RAM. Вектор из 2048 значений INTEGER занимает 8 КБ (2048 на 4 байта), BIGINT — 16 КБ; несколько векторов оператора вместе — десятки КБ, что укладывается в L1/L2. Если бы вектор был миллион значений, он занял бы мегабайты, не влез в кэш, и движок упирался бы в RAM (фактически полная материализация); если бы был 16 значений — накладные расходы амортизировались бы лишь в 16 раз. 2048 — точка равновесия: рабочий набор в кэше, накладные расходы амортизированы. Третье свойство — 2048 это степень двойки (2 в 11-й), что важно для SIMD: SIMD-инструкции обрабатывают несколько значений за такт, ширина SIMD-регистров тоже степень двойки (4, 8, 16), и цикл по 2048 значениям раскладывается на целое число SIMD-итераций без остатка-хвоста. Важная деталь: STANDARD_VECTOR_SIZE — это константа времени компиляции, зашитая в бинарник, а не рантайм-настройка; её нельзя и не нужно менять через SET, весь движок оптимизирован под 2048.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Между какими двумя противоположными требованиями зажат выбор размера вектора (STANDARD_VECTOR_SIZE)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7