2.6Kпросмотров
18 марта 2025 г.
Score: 2.8K
В умных книгах пишут, что к анализу производительности стоит подходить в несколько этапов: 1. определить сколько времени должна занимать работа;
2. измерить сколько работа занимает на самом деле;
3. выдвинуть гипотезу почему есть различия и что фиксить;
4. внести изменения в систему с целью привести (2) к (1);
5. GOTO (2) Если первый этап обычно стабилен и редко меняется, то основное внимание уделяется последующим шагам, при этом пункты с измерениями выглядят наиболее значимыми. И (возможно) самыми сложными. ——————————— Пример анализа потребления CPU (очень упрощенно) замерь общее потребление CPU на машине (top, vmstat); определи долю целевого процесса и её распределение на user/system mode (top, pidstat); найди самые "горячие" функции/методы/системные вызовы (perf, profile); изучи код, чтобы понять, что именно «бьет» по производительности. Когда узкое место найдено, принимай решение: оптимизировать логику, менять зависимости или что-то еще. И не забудь про мантры производительности. Теперь внеси изменения и замерь их аффект. А вот тут нас могут караулить неприятности: точно ли измерили то, что ожидали измерить? ——————————— В главе Measuring CPUs книги Understanding Software Dynamics разбирается случай измерения времени выполнения инструкции add в тактах процессора. В качестве решения "в лоб" автор приводит:
start = RDTSC();
for (int n = 0; n < 5000; ++n) {
sum += 1;
}
delta = RDTSC() - start; Здесь фиксируется начальное время (start), затем выполняется инкремент (add) в цикле, после чего рассчитывается разница между началом и концом операции (delta). Разделив полученную дельту на число итераций, автор получил 6.76 тактов процессора на один проход, что довольно дорого для такой "элементарной инструкции". (и это среднее значение, а значит, разброс по перцентилям может быть значительным) На этом можно было бы остановиться: "померяли же!", но если копнуть глубже (куда уж глубже🙂), то окажется, что на ассемблере цикл for{} раскладывается в:
cmpl $999999999, -44(%rbp) # сравнение i с константой
jg .L3 # условный переход, если i > константа
addq $1, -40(%rbp) # sum += 1; значение sum хранится в памяти по адресу -40(%rbp)
addl $1, -44(%rbp) # ++i; значение i хранится в памяти по адресу -44(%rbp)
jmp .L4 # переход к началу цикла Прямая речь:
[прим. alebsys: loop for{}] has five instructions, three of which access memory by three reads (cmpl, addq, addl) and two writes (addq, addl). So most of what we are measuring is in fact memory accesses, specifically to the L1 data cache. Далее автор рассматривает способы минимизировать оверхед от цикла и выходит на почти "чистый" замер add в 1.06 такта на инструкцию. А казалось бы "че там мерить, зашел вышел на пять минут"') ——————————— Кстати, не обязательно копаться так глубоко, чтобы столкнуться с подобными ошибками интерпретации, они встречаются повсеместно. Например, можно думать, что измеряем задержки на сети, хотя узкое место в локальной очереди на машине. Или радоваться быстрым дискам, хотя по факту запись шла асинхронно через файловую систему. P.S. Товарищ, будь бдителен!