rdtsc хронометраж для измерения функции
Я хочу, чтобы время вызова функции с rdtsc. Поэтому я измерил это двумя способами следующим образом.
- Назовите это в цикле. Объедините каждую разность rdtsc в цикле и разделите на количество вызовов. (Скажем, это N)
- Назовите это в цикле. Получите разность rdtsc самого цикла и разделите на N.
Но я вижу пару противоречивых поведений.
- Когда я увеличиваю N, времена как монотонно уменьшаются в обоих методах 1 и 2. Для метода 2 понятно, что он амортизирует накладные расходы на управление циклом. Но я не уверен, как это так для метода 1.
- Фактически для метода 2 каждый раз, когда я увеличиваю N, значение, которое я получаю для N=1, кажется, просто делится на новое N каждый раз. Проверка дизассемблирования GDB позволила мне понять, что это какая-то оптимизация компилятора в -O2, где цикл пропускается во втором случае. Поэтому я повторил попытку с -O0, где разборка GDB показывает фактический цикл, который был там для второго случая.
Код приведен ниже.
#include <stdio.h>
#include <inttypes.h>
#include <stdlib.h>
typedef unsigned long long ticks;
static __inline__ ticks getticks(void) {
unsigned a, d;
asm volatile("rdtsc" : "=a" (a), "=d" (d));
return ((ticks)a) | (((ticks)d) << 32);
}
__attribute__ ((noinline))
void bar() {
}
int main(int argc, char** argv) {
long long N = 1000000;
N = atoi(argv[1]);
int i;
long long bar_total = 0;
ticks start = 0, end = 0;
for (i = 0; i < N; i++) {
start = getticks();
bar();
end = getticks();
bar_total += (end - start);
}
fprintf(stdout, "Total invocations : %lld\n", N);
fprintf(stdout, "[regular] bar overhead : %lf\n", ((double)bar_total/ N));
start = getticks();
for (i = 0; i < N; i++) {
bar();
}
end = getticks();
bar_total = (end - start);
fprintf(stdout, "[Loop] bar overhead : %lf\n", ((double)bar_total/ N));
return 0;
}
Есть идеи, что здесь происходит? Я могу поставить разборку GDB, если это необходимо. Я использовал реализацию rdtsc из http://dasher.wustl.edu/tinker/distribution/fftw/kernel/cycle.h
Изменить: мне придется отказаться от моего второго утверждения, что при -O0 время уменьшается прямо пропорционально N во втором случае. Я предполагаю, что во время сборки я допустил ошибку, из-за которой сохранилась старая версия. Любой, как это все еще идет вниз вместе с рисунком для метода 1. Вот некоторые числа для различных значений N.
taskset -c 2 ./example.exe 1
Total invocations : 1
[regular] bar overhead : 108.000000
[Loop] bar overhead : 138.000000
taskset -c 2 ./example.exe 10
Total invocations : 10
[regular] bar overhead : 52.900000
[Loop] bar overhead : 40.700000
taskset -c 2 ./example.exe 100
Total invocations : 100
[regular] bar overhead : 46.780000
[Loop] bar overhead : 15.570000
taskset -c 2 ./example.exe 1000
Total invocations : 1000
[regular] bar overhead : 46.069000
[Loop] bar overhead : 13.669000
taskset -c 2 ./example.exe 100000
Total invocations : 10000
[regular] bar overhead : 46.010100
[Loop] bar overhead : 13.444900
taskset -c 2 ./example.exe 100000000
Total invocations : 100000000
[regular] bar overhead : 26.970272
[Loop] bar overhead : 5.201252
taskset -c 2 ./example.exe 1000000000
Total invocations : 1000000000
[regular] bar overhead : 18.853279
[Loop] bar overhead : 5.218234
taskset -c 2 ./example.exe 10000000000
Total invocations : 1410065408
[regular] bar overhead : 18.540719
[Loop] bar overhead : 5.216395
Теперь я вижу два новых поведения.
- Метод 1 сходится медленнее, чем метод 2. Но я все еще ломаю голову над тем, почему существует такая радикальная разница в значениях для разных настроек N. Возможно, я делаю здесь какую-то основную ошибку, которую сейчас не вижу.
- Значение метода 1 на самом деле больше, чем метод 2 на некоторый запас. Я ожидал, что он будет на одном уровне или немного меньше значения метода 2, поскольку он не содержит служебных данных управления циклом.
Вопросы
Итак, в заключение мои вопросы
Почему значения, данные обоими методами, так резко меняются при увеличении N? Специально для метода 1, который не учитывает накладные расходы на управление циклом.
Почему результат второго метода меньше, чем у первого метода, когда первый метод исключает накладные расходы управления циклом в вычислениях?
Редактировать 2
Относительно предложенного решения rdtscp.
Будучи непосвященным относительно встроенной сборки, я сделал следующее.
static __inline__ ticks getstart(void) {
unsigned cycles_high = 0, cycles_low = 0;
asm volatile ("CPUID\n\t"
"RDTSC\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
"%rax", "%rbx", "%rcx", "%rdx");
return ((ticks)cycles_high) | (((ticks)cycles_low) << 32);
}
static __inline__ ticks getend(void) {
unsigned cycles_high = 0, cycles_low = 0;
asm volatile("RDTSCP\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t"
"CPUID\n\t": "=r" (cycles_high), "=r" (cycles_low)::
"%rax", "%rbx", "%rcx", "%rdx");
return ((ticks)cycles_high) | (((ticks)cycles_low) << 32);
}
и использованные выше методы до и после вызова функции. Но теперь я получаю бессмысленные результаты, как следует.
Total invocations : 1000000
[regular] bar overhead : 304743228324.708374
[Loop] bar overhead : 33145641307.734016
В чем подвох? Я хотел выделить их как встроенные методы, так как вижу их использование в нескольких местах.
А. Решение в комментариях.
2 ответа
Вы используете простой rdtsc
инструкция, которая может работать некорректно на процессорах, вышедших из строя, таких как Xeons и Cores. Вы должны добавить некоторые инструкции по сериализации или переключиться на rdtscp
инструкция:
http://en.wikipedia.org/wiki/Time_Stamp_Counter
Начиная с Pentium Pro, процессоры Intel поддерживают выполнение вне очереди, где инструкции не обязательно выполняются в порядке их появления в исполняемом файле. Это может привести к тому, что RDTSC будет выполнен позже, чем ожидалось, что приведет к вводящему в заблуждение количеству циклов.[3] Эту проблему можно решить, выполнив команду сериализации, такую как CPUID, для принудительного завершения каждой предыдущей команды перед тем, как продолжить выполнение программы, или используя команду RDTSCP, которая является вариантом сериализации команды RDTSC.
Корпорация Intel недавно выпустила руководство по использованию rdtsc/rdtscp - Как оценить время выполнения кода на архитектурах наборов инструкций Intel IA-32 и IA-64 (ia-32-ia-64-benchmark-code-execute-paper.pdf, 324264-001 2010). Они рекомендуют cpuid+rdtsc для запуска и rdtscp для конечных таймеров:
Решение проблемы, представленной в разделе 0, заключается в добавлении инструкции CPUID сразу после
RDTPSCP
и дваmov
инструкции (чтобы сохранить в памяти значениеedx
а такжеeax
). Реализация заключается в следующем:
asm volatile ("CPUID\n\t"
"RDTSC\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
"%rax", "%rbx", "%rcx", "%rdx");
/***********************************/
/*call the function to measure here*/
/***********************************/
asm volatile("RDTSCP\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t"
"CPUID\n\t": "=r" (cycles_high1), "=r" (cycles_low1)::
"%rax", "%rbx", "%rcx", "%rdx");
start = ( ((uint64_t)cycles_high << 32) | cycles_low );
end = ( ((uint64_t)cycles_high1 << 32) | cycles_low1 );
В приведенном выше коде первый
CPUID
Вызов реализует барьер, чтобы избежать неупорядоченного выполнения инструкций выше и нижеRDTSC
инструкция. Тем не менее, этот вызов не влияет на измерение, так как он происходит доRDTSC
(т. е. до чтения регистра метки времени). ПервыйRDTSC
затем читает регистр метки времени, и значение сохраняется в памяти. Затем выполняется код, который мы хотим измерить. Если код является вызовом функции, рекомендуется объявить такую функцию какinline
", Так что с точки зрения сборки нет никаких накладных расходов при вызове самой функции.RDTSCP
инструкция во второй раз считывает регистр метки времени и гарантирует, что выполнение всего кода, который мы хотели измерить, завершено.
Ваш пример не очень правильный; вы пытаетесь измерить пустую функцию bar()
, но это так мало, что вы измеряете накладные расходы rdtsc в методе 1 (for() { rdtsc; bar(); rdtsc)
). В соответствии с таблицей Агвелла Фога для haswell - http://www.agner.org/optimize/instruction_tables.pdf стр. 191 (длинная таблица "Intel Haswell Список сроков выполнения команд и разбивки по мопам", в самом конце) RDTSC
имеет 15 мопов (слияние невозможно) и латентность 24 тика; RDTSCP
(для более старой микроархитектуры Sandy Bridge имеет задержку в 23 мопа и 36 тиков против 21 мопа и 28 тиков для rdtsc). Таким образом, вы не можете использовать обычный rdtsc (или rdtscp) для непосредственного измерения такого короткого кода.
Ты пытался clock_gettime(CLOCK_MONOTONIC, &tp)
? Должно быть достаточно близко к считыванию счетчика циклов вручную, также имейте в виду, что счетчик циклов может не синхронизироваться между ядрами процессора.