Профилирование на основе подсчета циклов процессора в C/C++ Linux x86_64
Я использую следующий код для профилирования своих операций для оптимизации циклов процессора, выполняемых в моих функциях.
static __inline__ unsigned long GetCC(void)
{
unsigned a, d;
asm volatile("rdtsc" : "=a" (a), "=d" (d));
return ((unsigned long)a) | (((unsigned long)d) << 32);
}
Я не думаю, что это лучше, потому что даже два последовательных звонка дают мне разницу "33". Какие-либо предложения?
6 ответов
Лично я считаю, что инструкция rdtsc хороша и может использоваться для самых разных задач. Я не думаю, что использование cpuid необходимо для подготовки к rdtsc. Вот как я рассуждаю вокруг rdtsc:
- Поскольку я использую компилятор Watcom, я реализовал rdtsc, используя "#pragma aux", что означает, что компилятор C будет генерировать встроенную инструкцию, ожидать результат в edx:eax и также сообщать оптимизатору, что содержимое eax и edx было модифицирована. Это огромное улучшение по сравнению с традиционными реализациями _asm, где оптимизатор будет избегать оптимизации в непосредственной близости от _asm. Я также реализовал div_U8_by_U4, используя "#pragma aux", так что мне не нужно вызывать функцию lib, когда я конвертирую clock_cycles в нас или в ms.
- Каждое выполнение rdtsc приведет к некоторым издержкам (НАМНОГО больше, если оно инкапсулировано, как в примере автора), что должно быть принято во внимание, чем короче последовательность для измерения. Обычно я не измеряю время более коротких последовательностей, чем 1/30 внутренней тактовой частоты, которая обычно составляет 1/10^8 секунд (3 ГГц внутренние часы). Я использую такие измерения как показания, а не факт. Зная это, я могу опустить процессор. Чем больше раз я буду измерять, тем ближе буду к факту.
- Для надежного измерения я бы использовал диапазон 1/100 - 1/300, то есть 0,03 - 0,1 мкс. В этом диапазоне дополнительная точность использования cpuid практически невелика. Я использую этот диапазон для определения короткой последовательности. Это мой "нестандартный" модуль, так как он зависит от внутренней тактовой частоты процессора. Например, на машине с тактовой частотой 1 ГГц я бы не использовал 0,03 доллара США, потому что это поставило бы меня за пределы 1/100, и мои показания стали бы показаниями. Здесь я бы использовал 0.1 us в качестве единицы измерения кратчайшего времени. 1/300 не будет использоваться, поскольку он будет слишком близок к 1 мкс (см. Ниже), чтобы иметь какое-либо существенное значение.
- Для еще более длинных последовательностей обработки я делю разницу между двумя показаниями rdtsc, скажем, 3000 (для 3 ГГц), и преобразую нам прошедшие такты. На самом деле я использую (diff+1500)/3000, где 1500 - половина 3000. Для ожидания ввода-вывода я использую миллисекунды => (diff+1500000)/3000000. Это мои "стандартные" единицы. Я очень редко использую секунды.
- Иногда я получаю неожиданно медленные результаты, и тогда я должен спросить себя: это из-за прерывания или из-за кода? Я измеряю еще несколько раз, чтобы увидеть, действительно ли это было прерывание. В этом случае... ну, в реальном мире постоянно происходят прерывания. Если моя последовательность короткая, то есть вероятность, что следующее измерение не будет прервано. Если последовательность длиннее, прерывания будут происходить чаще, и я ничего не могу с этим поделать.
- Очень точное измерение длительности прошедшего времени (у нас и более часовые и более длинные ET) увеличат риск получения исключения деления в Div_U8_by_U4, поэтому я думаю, когда использовать нас и когда использовать мс.
- У меня также есть код для базовой статистики. Используя это, я записываю минимальные и максимальные значения и могу вычислить среднее и стандартное отклонение. Этот код нетривиален, поэтому его собственный ET должен быть вычтен из измеренных ET.
- Если компилятор выполняет обширную оптимизацию и ваши показания хранятся в локальных переменных, компилятор может определить ("правильно"), что код можно опустить. Один из способов избежать этого - хранить результаты в открытых (нестатических, не на основе стека) переменных.
- Программы, работающие в реальных условиях, должны измеряться в реальных условиях, и тут нет пути.
Что касается точности счетчика меток времени, я бы сказал, что при условии синхронизации tsc на разных ядрах (что является нормой) существует проблема дросселирования ЦП в периоды низкой активности для снижения энергопотребления. При тестировании всегда можно заблокировать функциональность. Если вы выполняете инструкцию на частоте 1 ГГц или 10 МГц на одном и том же процессоре, отсчет истекшего цикла будет таким же, даже если первое выполнено за 1% времени по сравнению с последним.
Попытка подсчитать циклы отдельного выполнения функции не совсем верный путь. Тот факт, что ваш процесс может быть прерван в любое время, наряду с задержками, вызванными ошибками в кэше и неправильными прогнозами ветвлений, означает, что может быть значительное отклонение в количестве циклов, взятых от вызова к вызову.
Правильный путь:
- Подсчитайте количество циклов или процессорного времени (с
clock()
) принимается большое количество обращений к функции, затем усредняется их; или же - Используйте эмулирующий профилировщик на уровне цикла, такой как Callgrind / kcachegrind.
Кстати, перед сериализацией нужно выполнить инструкцию RDTSC
, типично CPUID
используется.
Еще одна вещь, о которой вам может понадобиться беспокоиться: если вы работаете на многоядерном компьютере, программа может быть перенесена на другое ядро с другим счетчиком rdtsc. Вы можете закрепить процесс за одним ядром через системный вызов.
Если бы я пытался измерить что-то вроде этого, я, вероятно, записал бы метки времени в массив, а затем вернулся бы и проверил этот массив после того, как тестируемый код завершился. При изучении данных, записанных в массив временных отметок, вы должны иметь в виду, что этот массив будет опираться на кэш-память ЦП (и, возможно, на пейджинг, если ваш массив большой), но вы можете предварительно извлечь или просто помнить об этом, анализируя данные. Вы должны видеть очень регулярную дельту времени между отметками времени, но с несколькими всплесками и, возможно, несколькими провалами (вероятно, из-за перемещения в другое ядро). Регулярная дельта времени, вероятно, является вашим лучшим измерением, поскольку предполагает, что никакие внешние события не повлияли на эти измерения.
При этом, если код, который вы тестируете, имеет нерегулярные шаблоны доступа к памяти или время выполнения или использует системные вызовы (особенно связанные с IO), тогда вам будет трудно отделить шум от интересующих вас данных.
Вы находитесь на правильном пути1, но вам нужно сделать две вещи:
- Бежать
cpuid
инструкция передrdtsc
очистить конвейер ЦП (делает измерение более надежным). Насколько я помню это клоббер регистрируется изeax
вedx
, - Мера в реальном времени. Время выполнения намного больше, чем просто циклы ЦП (конкуренция за блокировку, переключение контекста и другие издержки, которые вы не контролируете). Калибруйте тики TSC в режиме реального времени. Вы можете сделать это в простом цикле, который принимает различия в измерениях, скажем,
gettimeofday
(Linux, так как вы не упомянули платформу) звонки иrdtsc
выход. Затем вы можете сказать, сколько времени занимает каждый тик TSC. Другим соображением является синхронизация TSC между процессорами, поскольку каждое ядро может иметь свой собственный счетчик. В Linux вы можете увидеть это в/proc/cpuinfo
ваш процессор должен иметьconstant_tsc
флаг. Большинство новых процессоров Intel, которые я видел, имеют этот флаг.
1 я лично нашел rdtsc
быть более точным, чем системные вызовы, такие как gettimeofday()
для мелкозернистых измерений.
Linux
perf_event_open
системный вызов с
config = PERF_COUNT_HW_CPU_CYCLES
Этот системный вызов Linux представляет собой кросс-архитектурную оболочку для событий производительности.
Этот ответ в основном совпадает с ответом на этот вопрос C++: как получить количество циклов процессора в x86_64 из C++? см. этот ответ для получения более подробной информации.
perf_event_open.c
#include <asm/unistd.h>
#include <linux/perf_event.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <inttypes.h>
static long
perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
int cpu, int group_fd, unsigned long flags)
{
int ret;
ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
group_fd, flags);
return ret;
}
int
main(int argc, char **argv)
{
struct perf_event_attr pe;
long long count;
int fd;
uint64_t n;
if (argc > 1) {
n = strtoll(argv[1], NULL, 0);
} else {
n = 10000;
}
memset(&pe, 0, sizeof(struct perf_event_attr));
pe.type = PERF_TYPE_HARDWARE;
pe.size = sizeof(struct perf_event_attr);
pe.config = PERF_COUNT_HW_CPU_CYCLES;
pe.disabled = 1;
pe.exclude_kernel = 1;
// Don't count hypervisor events.
pe.exclude_hv = 1;
fd = perf_event_open(&pe, 0, -1, -1, 0);
if (fd == -1) {
fprintf(stderr, "Error opening leader %llx\n", pe.config);
exit(EXIT_FAILURE);
}
ioctl(fd, PERF_EVENT_IOC_RESET, 0);
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
/* Loop n times, should be good enough for -O0. */
__asm__ (
"1:;\n"
"sub $1, %[n];\n"
"jne 1b;\n"
: [n] "+r" (n)
:
:
);
ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
read(fd, &count, sizeof(long long));
printf("%lld\n", count);
close(fd);
}
TSC не является хорошей мерой времени. Единственная гарантия, которую процессор дает для TSC, - это то, что он монотонно растет (то есть, если вы RDTSC
один раз и затем сделайте это снова, второй вернет результат, который выше, чем первый), и что потребуется много времени, чтобы обернуть его.
Правильно ли я понимаю, что причина, по которой вы это делаете, заключается в том, чтобы заключить в скобки другой код, чтобы вы могли измерить, сколько времени занимает другой код?
Я уверен, что вы знаете еще один хороший способ сделать это - просто зациклить другой код 10^6 раз, остановить его и назвать его микросекундами.
После того как вы измерили другой код, могу ли я предположить, что вы хотите знать, какие строки в нем стоит оптимизировать, чтобы сократить время, которое требуется?
Если это так, то вы на правильном пути. Вы можете использовать такой инструмент, как Zoom или LTProf. Вот мой любимый метод.