voidbytes.dev

системное программирование вслух: аллокаторы, сисколлы, профиль, разбор падений - и места, где я сам намерил неправильно

Чем я мерю

Короткий список того, что реально открывается каждую неделю, и правила, в каком порядке я это делаю. Ничего экзотического: всё перечисленное либо входит в систему, либо ставится одной командой.

Стенд

Все числа в записях получены на одном из двух узлов, и я каждый раз указываю, на каком.

узелсостав
x86-648 ядер, 32 ГиБ, Linux 6.6, glibc 2.39, gcc 13.2
aarch644 ядра, 8 ГиБ, Linux 6.6, glibc 2.39, gcc 13.2

Перед замером фиксирую частоту: регулятор в performance, турбо выключено. Без этого разброс между прогонами доходит до 15 %, и любая правка на пять процентов становится неотличима от шума. Пять прогонов, беру медиану, первый прогон выбрасываю как прогрев.

Порядок, а не набор

  1. Счётчики. perf stat: такты, инструкции, IPC, промахи кеша, переключения контекста. Это три-четыре числа, которые говорят, чего программа ждёт - вычислений, памяти или ядра.
  2. Профиль. perf record -e cycles:pp -g, и только после того, как счётчики показали, что узкое место вообще в пользовательском коде.
  3. Состав обращений к ядру. strace -f -c один раз, чтобы увидеть, какие вызовы есть. Дальше считаю их точками трассировки, потому что ptrace замедляет объект на порядок.
  4. Память. mallinfo2() изнутри и /proc/<pid>/smaps_rollup снаружи.

Нарушение этого порядка стоило мне трёх дней в разборе про memcpy: профиль был снят первым и ответил не на тот вопрос.

Сборка

# рабочая сборка, в которой я и мерю
-O2 -g -fno-omit-frame-pointer

# отдельная сборка для проверок, гоняется до слияния ветки
-O1 -g -fsanitize=address,undefined,alignment

# компоновка с типами, ловит несовпадение прототипов между файлами
-flto -Wlto-type-mismatch

-fno-omit-frame-pointer в сборке для замеров обязателен: без него стек в профиле собирается эвристиками и врёт на вложенных вызовах. Потеря скорости от этого на моих задачах в пределах процента, то есть меньше разброса между прогонами.

Отладка падений

Корки включены на всех стендах: core_pattern пишет файл с номером процесса и временем, лимит снят. В gdb первыми тремя командами всегда идут bt, x/i $pc и печать регистра, который участвует в упавшей инструкции - этого хватило бы в истории с SIGBUS, если бы я посмотрел туда сразу.

Дальше по надобности: objdump -d для одной функции, readelf -S когда вопрос про секции и выравнивание, nm когда символ не тот, addr2line для адреса из журнала.

Чего у меня нет в наборе

Одна мелочь про perf

На большинстве машин perf_event_paranoid выставлен так, что счётчики недоступны непривилегированному процессу. Я поднимаю права на время замера и возвращаю значение обратно сразу после - оставлять открытым не стоит, а забыть вернуть очень легко, поэтому у меня это одна строка в скрипте замера, которая делает и то и другое.

Флаги, которыми собираю

ЗачемФлаги
замер скорости-O2 -g -fno-omit-frame-pointer
поиск UB и переполнений-O1 -g -fsanitize=address,undefined,alignment
гонки данных-O1 -g -fsanitize=thread
перед слиянием ветки-flto -Wlto-type-mismatch

-fno-omit-frame-pointer здесь не прихоть: без него perf -g на -O2 строит стек по эвристике и врёт, а эти несколько процентов скорости не стоят испорченного профиля. Санитайзеры между собой не дружат: ASan и TSan вместе не собираются, поэтому прогоны раздельные.

Какие счётчики за что отвечают

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

СчётчикЧто ловитЗапись
LLC-load-missesпромахи кеша данныхпрофиль врёт
dTLB-load-missesобход таблицы страницпромахи TLB
futex в strace -cожидание на блокировкефьютекс в профиле
perf c2c, HITMложное разделение линийложное разделение
insn per cycleпростаивает ли конвейер0,32 IPC

Короткий словарь

HITM
hit-modified: ядро читает кеш-линию, которую другое ядро только что изменило - метка ложного разделения или честной конкуренции за данные.
walk_active
такты, в которые MMU обходит таблицу страниц после промаха TLB; видно отдельным счётчиком, в профиле по символам размазано.
off-CPU
время, когда поток спит, а не считает; профиль по циклам его не видит, и хвосты задержки живут именно здесь.
RSS
резидентная память процесса; после free не обязана падать - страницы остаются за процессом до malloc_trim или давления памяти.