Почему clock_gettime так неустойчив?
Введение
-
В разделе "Старый вопрос" содержится первоначальный вопрос (добавлено дополнительное исследование и заключение).
-
Перейти к разделу Дальнейшее исследование ниже для детального сравнения различных методов синхронизации (rdtsc
, clock_gettime
и QueryThreadCycleTime
).
-
Я считаю, что ошибочное поведение CGT можно отнести либо к багги-ядру, либо к багги-процессору (см. раздел "Заключение" ).
-
Код, используемый для тестирования, находится в нижней части этого вопроса (см. раздел Приложение).
-
Извините за длину.
Старый вопрос
Короче: я использую clock_gettime
для измерения времени выполнения многих сегментов кода. Я испытываю очень несовместимые измерения между отдельными прогонами. Метод имеет чрезвычайно высокое стандартное отклонение по сравнению с другими методами (см. Пояснение ниже).
Вопрос: Есть ли причина, по которой clock_gettime
дает такие противоречивые измерения по сравнению с другими методами? Есть ли альтернативный метод с тем же разрешением, который учитывает время простоя потока?
Объяснение. Я пытаюсь профилировать несколько небольших частей кода C. Время выполнения каждого из сегментов кода составляет не более нескольких микросекунд. За один проход каждый сегмент кода будет выполняться несколько сотен раз, что производит измерения runs × hundreds
.
Я также должен измерять только время, в течение которого поток фактически проводит выполнение (поэтому rdtsc
не подходит). Мне также требуется высокое разрешение (поэтому times
не подходит).
Я пробовал следующие методы:
-
rdtsc
(в Linux и Windows),
-
clock_gettime
(с 'CLOCK_THREAD_CPUTIME_ID', в Linux) и
-
QueryThreadCycleTime
(в Windows).
Методология. Анализ проводился на 25 прогонах. В каждом прогоне отдельные сегменты кода повторяют 101 раз. Поэтому у меня 2525 измерений. Затем я смотрю на гистограмму измерений, а также вычисляю некоторые основные вещи (например, среднее значение, std.dev., Медиана, режим, мин и макс).
Я не представляю, как я измерил "сходство" трех методов, но это просто включало базовое сравнение доли времени, проведенного в каждом сегменте кода ( "пропорция" означает, что времена нормализованы). Затем я смотрю на чистые различия в этих пропорциях. Это сравнение показало, что все "rdtsc", "QTCT" и "CGT" измеряют одинаковые пропорции при усреднении по 25 прогонам. Однако приведенные ниже результаты показывают, что "CGT" имеет очень большое стандартное отклонение. Это делает его непригодным для использования в моем случае использования.
Результаты
Сравнение clock_gettime
с rdtsc
для одного и того же сегмента кода (25 прогонов 101 измерения = 2525 отсчетов):
Обсуждение
-
rdtsc
дает очень похожие результаты как для Linux, так и для Windows. Он имеет приемлемое стандартное отклонение - он на самом деле довольно устойчив/стабилен. Однако он не учитывает время простоя потока. Таким образом, переключатели контекста делают измерения неустойчивыми (в Windows я наблюдал это довольно часто: сегмент кода со средним числом 1000 тиков или около того будет принимать ~ 30000 тиков каждый раз, а затем - определенно из-за предварительного извлечения).
-
QueryThreadCycleTime
дает очень последовательные измерения - т.е. значительно меньшее стандартное отклонение по сравнению с rdtsc
. Когда никаких переключений контекста не происходит, этот метод почти идентичен rdtsc
.
-
clock_gettime
, с другой стороны, создает чрезвычайно противоречивые результаты (не только между прогонами, но и между измерениями). Стандартные отклонения являются экстремальными (по сравнению с rdtsc
).
Я надеюсь, что статистика в порядке. Но что может быть причиной такого расхождения в измерениях между этими двумя методами? Конечно, есть кэширование, миграция CPU/core и другие вещи. Но ничто из этого не должно отвечать за любые такие различия между "rdtsc" и "clock_gettime". Что происходит?
Дальнейшее исследование
Я исследовал это немного дальше. Я сделал две вещи:
-
Измерение накладных расходов при вызове clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t)
(см. код 1 в Приложении) и
-
в обычном цикле, называемом clock_gettime
, и сохранил показания в массиве (см. код 2 в Приложении). Я измеряю времена дельта (разницу в последовательных временах измерения, которые должны немного соответствовать служебным запросам вызова clock_gettime
).
Я измерил его на двух разных компьютерах с двумя разными версиями ядра Linux:
ВКТ
-
CPU: Core 2 Duo L9400 @1,86 ГГц
Ядро: Linux 2.6.40-4.fc15.i686 # 1 SMP Пт Июл 29 18:54:39 UTC 2011 i686 i686 i386
Результаты
- Оценочные накладные расходы
clock_gettime
: между 690-710 нс
-
Дельта раз:
- В среднем: 815.22 нс
- Медиана: 713 нс
- Режим: 709 нс
- Мин.: 698 нс
- Максимум: 23359 нс
-
Гистограмма (левые диапазоны имеют частоты 0):
Range | Frequency
------------------+-----------
697 < x ≤ 800 -> 78111 <-- cached?
800 < x ≤ 1000 -> 16412
1000 < x ≤ 1500 -> 3
1500 < x ≤ 2000 -> 4836 <-- uncached?
2000 < x ≤ 3000 -> 305
3000 < x ≤ 5000 -> 161
5000 < x ≤ 10000 -> 105
10000 < x ≤ 15000 -> 53
15000 < x ≤ 20000 -> 8
20000 < x -> 5
-
CPU: 4-процессорный двухпроцессорный процессор AMD Opteron 275
Ядро: Linux 2.6.26-2-amd64 # 1 SMP Sun Jun 20 20:16:30 UTC 2010 x86_64 GNU/Linux
Результаты
- Оценочные накладные расходы
clock_gettime
: между 279-283 нс
-
Дельта раз:
- В среднем: 320.00
- Медиана: 1
- Режим: 1
- Мин: 1
- Макс: 3495529
-
Гистограмма (левые диапазоны имеют частоты 0):
Range | Frequency
--------------------+-----------
x ≤ 1 -> 86738 <-- cached?
282 < x ≤ 300 -> 13118 <-- uncached?
300 < x ≤ 440 -> 78
2000 < x ≤ 5000 -> 52
5000 < x ≤ 30000 -> 5
3000000 < x -> 8
RDTSC
Связанный код rdtsc_delta.c
и rdtsc_overhead.c
.
-
CPU: Core 2 Duo L9400 @1,86 ГГц
Ядро: Linux 2.6.40-4.fc15.i686 # 1 SMP Пт Июл 29 18:54:39 UTC 2011 i686 i686 i386
Результаты
- Оценочные накладные расходы: между 39-42 тиками
-
Дельта раз:
- В среднем: 52.46 тиков
- Медиана: 42 тика
- Режим: 42 тика
- Мин: 35 тиков
- Макс: 28700 тиков
-
Гистограмма (левые диапазоны имеют частоты 0):
Range | Frequency
------------------+-----------
34 < x ≤ 35 -> 16240 <-- cached?
41 < x ≤ 42 -> 63585 <-- uncached? (small difference)
48 < x ≤ 49 -> 19779 <-- uncached?
49 < x ≤ 120 -> 195
3125 < x ≤ 5000 -> 144
5000 < x ≤ 10000 -> 45
10000 < x ≤ 20000 -> 9
20000 < x -> 2
-
CPU: 4-процессорный двухпроцессорный процессор AMD Opteron 275
Ядро: Linux 2.6.26-2-amd64 # 1 SMP Sun Jun 20 20:16:30 UTC 2010 x86_64 GNU/Linux
Результаты
- Оценочные накладные расходы: от 13,7 до 17,0 тиков
-
Дельта раз:
- В среднем: 35,44 тика
- Медиана: 16 тиков
- Режим: 16 тиков
- Мин: 14 тиков
- Макс: 16372 тиков
-
Гистограмма (левые диапазоны имеют частоты 0):
Range | Frequency
------------------+-----------
13 < x ≤ 14 -> 192
14 < x ≤ 21 -> 78172 <-- cached?
21 < x ≤ 50 -> 10818
50 < x ≤ 103 -> 10624 <-- uncached?
5825 < x ≤ 6500 -> 88
6500 < x ≤ 8000 -> 88
8000 < x ≤ 10000 -> 11
10000 < x ≤ 15000 -> 4
15000 < x ≤ 16372 -> 2
QTCT
Связанный код qtct_delta.c
и qtct_overhead.c
.
Заключение
Я считаю, что ответ на мой вопрос будет ошибкой на моей машине (той, которая имеет процессоры AMD со старым ядром Linux).
Результаты CGT машины AMD со старым ядром показывают некоторые крайние показания. Если мы посмотрим на времена дельты, мы увидим, что наиболее частая дельта равна 1 нс. Это означает, что вызов clock_gettime
занял меньше, чем наносекунда! Более того, он также произвел ряд необычайно больших дельт (более 3000000 нс)! Это кажется ошибочным поведением. (Возможно, неучтенные миграции ядра?)
Примечания:
-
Накладные расходы CGT и QTCT довольно большие.
-
Также сложно учесть их накладные расходы, потому что кэширование процессора, по-видимому, имеет большое значение.
-
Возможно, придерживаться RDTSC, блокируя процесс в одном ядре и назначая приоритет в реальном времени, является самым точным способом узнать, сколько циклов использует фрагмент кода...
Приложение
Код 1: clock_gettime_overhead.c
#include <time.h>
#include <stdio.h>
#include <stdint.h>
/* Compiled & executed with:
gcc clock_gettime_overhead.c -O0 -lrt -o clock_gettime_overhead
./clock_gettime_overhead 100000
*/
int main(int argc, char **args) {
struct timespec tstart, tend, dummy;
int n, N;
N = atoi(args[1]);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tstart);
for (n = 0; n < N; ++n) {
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
}
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tend);
printf("Estimated overhead: %lld ns\n",
((int64_t) tend.tv_sec * 1000000000 + (int64_t) tend.tv_nsec
- ((int64_t) tstart.tv_sec * 1000000000
+ (int64_t) tstart.tv_nsec)) / N / 10);
return 0;
}
Код 2: clock_gettime_delta.c
#include <time.h>
#include <stdio.h>
#include <stdint.h>
/* Compiled & executed with:
gcc clock_gettime_delta.c -O0 -lrt -o clock_gettime_delta
./clock_gettime_delta > results
*/
#define N 100000
int main(int argc, char **args) {
struct timespec sample, results[N];
int n;
for (n = 0; n < N; ++n) {
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &sample);
results[n] = sample;
}
printf("%s\t%s\n", "Absolute time", "Delta");
for (n = 1; n < N; ++n) {
printf("%lld\t%lld\n",
(int64_t) results[n].tv_sec * 1000000000 +
(int64_t)results[n].tv_nsec,
(int64_t) results[n].tv_sec * 1000000000 +
(int64_t) results[n].tv_nsec -
((int64_t) results[n-1].tv_sec * 1000000000 +
(int64_t)results[n-1].tv_nsec));
}
return 0;
}
Код 3: rdtsc.h
static uint64_t rdtsc() {
#if defined(__GNUC__)
# if defined(__i386__)
uint64_t x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
return x;
# elif defined(__x86_64__)
uint32_t hi, lo;
__asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)lo) | ((uint64_t)hi << 32);
# else
# error Unsupported architecture.
# endif
#elif defined(_MSC_VER)
return __rdtsc();
#else
# error Other compilers not supported...
#endif
}
Код 4: rdtsc_delta.c
#include <stdio.h>
#include <stdint.h>
#include "rdtsc.h"
/* Compiled & executed with:
gcc rdtsc_delta.c -O0 -o rdtsc_delta
./rdtsc_delta > rdtsc_delta_results
Windows:
cl -Od rdtsc_delta.c
rdtsc_delta.exe > windows_rdtsc_delta_results
*/
#define N 100000
int main(int argc, char **args) {
uint64_t results[N];
int n;
for (n = 0; n < N; ++n) {
results[n] = rdtsc();
}
printf("%s\t%s\n", "Absolute time", "Delta");
for (n = 1; n < N; ++n) {
printf("%lld\t%lld\n", results[n], results[n] - results[n-1]);
}
return 0;
}
Код 5: rdtsc_overhead.c
#include <time.h>
#include <stdio.h>
#include <stdint.h>
#include "rdtsc.h"
/* Compiled & executed with:
gcc rdtsc_overhead.c -O0 -lrt -o rdtsc_overhead
./rdtsc_overhead 1000000 > rdtsc_overhead_results
Windows:
cl -Od rdtsc_overhead.c
rdtsc_overhead.exe 1000000 > windows_rdtsc_overhead_results
*/
int main(int argc, char **args) {
uint64_t tstart, tend, dummy;
int n, N;
N = atoi(args[1]);
tstart = rdtsc();
for (n = 0; n < N; ++n) {
dummy = rdtsc();
dummy = rdtsc();
dummy = rdtsc();
dummy = rdtsc();
dummy = rdtsc();
dummy = rdtsc();
dummy = rdtsc();
dummy = rdtsc();
dummy = rdtsc();
dummy = rdtsc();
}
tend = rdtsc();
printf("%G\n", (double)(tend - tstart)/N/10);
return 0;
}
Код 6: qtct_delta.c
#include <stdio.h>
#include <stdint.h>
#include <Windows.h>
/* Compiled & executed with:
cl -Od qtct_delta.c
qtct_delta.exe > windows_qtct_delta_results
*/
#define N 100000
int main(int argc, char **args) {
uint64_t ticks, results[N];
int n;
for (n = 0; n < N; ++n) {
QueryThreadCycleTime(GetCurrentThread(), &ticks);
results[n] = ticks;
}
printf("%s\t%s\n", "Absolute time", "Delta");
for (n = 1; n < N; ++n) {
printf("%lld\t%lld\n", results[n], results[n] - results[n-1]);
}
return 0;
}
Код 7: qtct_overhead.c
#include <stdio.h>
#include <stdint.h>
#include <Windows.h>
/* Compiled & executed with:
cl -Od qtct_overhead.c
qtct_overhead.exe 1000000
*/
int main(int argc, char **args) {
uint64_t tstart, tend, ticks;
int n, N;
N = atoi(args[1]);
QueryThreadCycleTime(GetCurrentThread(), &tstart);
for (n = 0; n < N; ++n) {
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
}
QueryThreadCycleTime(GetCurrentThread(), &tend);
printf("%G\n", (double)(tend - tstart)/N/10);
return 0;
}
#include <stdio.h>
#include <stdint.h>
#include <Windows.h>
/* Compiled & executed with:
cl -Od qtct_overhead.c
qtct_overhead.exe 1000000
*/
int main(int argc, char **args) {
uint64_t tstart, tend, ticks;
int n, N;
N = atoi(args[1]);
QueryThreadCycleTime(GetCurrentThread(), &tstart);
for (n = 0; n < N; ++n) {
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
QueryThreadCycleTime(GetCurrentThread(), &ticks);
}
QueryThreadCycleTime(GetCurrentThread(), &tend);
printf("%G\n", (double)(tend - tstart)/N/10);
return 0;
}
Ответы
Ответ 1
Ну, как CLOCK_THREAD_CPUTIME_ID
реализуется с помощью rdtsc
, он, вероятно, будет страдать от тех же проблем, что и он. На странице руководства для clock_gettime
говорится:
Часы CLOCK_PROCESS_CPUTIME_ID и CLOCK_THREAD_CPUTIME_IDреализованы на многих платформах с использованием таймеров от CPU (TSC on i386, AR.ITC на Itanium). Эти регистры могут различаться между ЦП и как следствие, эти часы могут возвращать фиктивные результаты, если процесс переносится в другой процессор.
Как это звучит, это может объяснить ваши проблемы? Может быть, вы должны заблокировать свой процесс на одном CPU, чтобы получить стабильные результаты?
Ответ 2
Когда у вас сильно искаженное распределение, которое не может пойти отрицательно, вы увидите большие расхождения между средним, медианным и режимом.
Стандартное отклонение не имеет смысла для такого распределения.
Как правило, хорошая идея - преобразовать его в лог файл.
Это сделает его "более нормальным".