В последних процессорах (по крайней мере, в последние десять лет) Intel предложила три специализированных счетчика производительности с фиксированной функциональностью в дополнение к различным настраиваемым счетчикам производительности. Три фиксированных счетчика:
INST_RETIRED.ANY
CPU_CLK_UNHALTED.THREAD
CPU_CLK_UNHALTED.REF_TSC
Первый подсчитывает отставку инструкций, второе число фактических циклов, а последнее - то, что нас интересует. Описание тома 3 руководства Intel Software Developers:
Это событие подсчитывает количество эталонных циклов при скорости TSC, когда ядро не находится в состоянии остановки, а не в состоянии остановки TM. ядро входит в состояние остановки, когда он запускает инструкцию HLT или инструкция MWAIT. На это событие не влияет частота ядра (например, P-состояния), но рассчитывается с той же частотой, что и время счетчик штампов. Это событие может приблизиться к прошедшему времени, в то время как ядро не находится в состоянии остановки, а не в состоянии остановки TM.
Итак, для цикла, привязанного к процессору, я ожидаю, что это значение будет таким же, как и свободное значение TSC, считанное с rdstc
, так как они должны расходиться только для инструкций с остановками циклов или того, что означает состояние остановки в TM.
Я тестирую это с помощью следующего цикла (полная отдельная демонстрация доступна на github):
for (int i = 0; i < 100; i++) {
PFC_CNT cnt[7] = {};
int64_t start = nanos();
PFCSTART(cnt);
int64_t tsc =__rdtsc();
busy_loop(CALIBRATION_LOOPS);
PFCEND(cnt);
int64_t tsc_delta = __rdtsc() - tsc;
int64_t nanos_delta = nanos() - start;
printf(CPU_W "d" REF_W ".2f" TSC_W ".2f" MHZ_W ".2f" RAT_W ".6f\n",
sched_getcpu(),
1000.0 * cnt[PFC_FIXEDCNT_CPU_CLK_REF_TSC] / nanos_delta,
1000.0 * tsc_delta / nanos_delta,
1000.0 * CALIBRATION_LOOPS / nanos_delta,
1.0 * cnt[PFC_FIXEDCNT_CPU_CLK_REF_TSC]/tsc_delta);
}
Единственная важная вещь в приуроченном регионе - это busy_loop(CALIBRATION_LOOPS);
, который представляет собой просто замкнутый цикл энергозависимых хранилищ, который как скомпилированный gcc
и clang
выполняется за один цикл за итерацию на новейшем оборудовании:
void busy_loop(uint64_t iters) {
volatile int sink;
do {
sink = 0;
} while (--iters > 0);
(void)sink;
}
Команды PFCSTART
и PFCEND
читают счетчик CPU_CLK_UNHALTED.REF_TSC
, используя libpfc. __rdtsc()
является внутренним, который считывает TSC с помощью команды rdtsc
. Наконец, мы измеряем реальное время с помощью nanos()
, который просто:
int64_t nanos() {
auto t = std::chrono::high_resolution_clock::now();
return std::chrono::time_point_cast<std::chrono::nanoseconds>(t).time_since_epoch().count();
}
Да, я не выдаю cpuid
, и вещи не чередуются точно, но цикл калибровки является полной секундой, поэтому такие проблемы с наносекундным масштабом просто разбавляются до более или менее ничего.
С включенным TurboBoost, вот первые несколько результатов от обычного запуска:
CPU# REF_TSC rdtsc Eff Mhz Ratio
0 2392.05 2591.76 2981.30 0.922946
0 2381.74 2591.79 3032.86 0.918955
0 2399.12 2591.79 3032.50 0.925660
0 2385.04 2591.79 3010.58 0.920230
0 2378.39 2591.79 3010.21 0.917663
0 2355.84 2591.77 2928.96 0.908970
0 2364.99 2591.79 2942.32 0.912492
0 2339.64 2591.77 2935.36 0.902720
0 2366.43 2591.79 3022.08 0.913049
0 2401.93 2591.79 3023.52 0.926747
0 2452.87 2591.78 3070.91 0.946400
0 2350.06 2591.79 2961.93 0.906733
0 2340.44 2591.79 2897.58 0.903020
0 2403.22 2591.79 2944.77 0.927246
0 2394.10 2591.79 3059.58 0.923723
0 2359.69 2591.78 2957.79 0.910449
0 2353.33 2591.79 2916.39 0.907992
0 2339.58 2591.79 2951.62 0.902690
0 2395.82 2591.79 3017.59 0.924389
0 2353.47 2591.79 2937.82 0.908047
Здесь REF_TSC
является фиксированным счетчиком производительности TSC, как описано выше, и rdtsc
является результатом команды rdtsc
. Eff Mhz
- это эффективная рассчитанная истинная частота процессора в течение интервала и в основном показана ради любопытства и в качестве быстрого подтверждения того, сколько турбо вступает. Ratio
- это отношение столбцов REF_TSC
и rdtsc
. Я ожидаю, что это будет очень близко к 1, но на практике мы видим, что он колеблется от 0,90 до 0,92 с большой дисперсией (я видел его как 0,8 на других прогонах).
Графически это выглядит примерно так: 2:
Вызов rdstc
возвращает почти точные результаты 1 тогда как счетчик TSC PMU повсюду, иногда почти как 2300 МГц.
Если я отключить турбо, результаты гораздо более согласованы:
CPU# REF_TSC rdtsc Eff Mhz Ratio
0 2592.26 2592.25 2588.30 1.000000
0 2592.26 2592.26 2591.11 1.000000
0 2592.26 2592.26 2590.40 1.000000
0 2592.25 2592.25 2590.43 1.000000
0 2592.26 2592.26 2590.75 1.000000
0 2592.26 2592.26 2590.05 1.000000
0 2592.25 2592.25 2590.04 1.000000
0 2592.24 2592.24 2590.86 1.000000
0 2592.25 2592.25 2590.35 1.000000
0 2592.25 2592.25 2591.32 1.000000
0 2592.25 2592.25 2590.63 1.000000
0 2592.25 2592.25 2590.87 1.000000
0 2592.25 2592.25 2590.77 1.000000
0 2592.25 2592.25 2590.64 1.000000
0 2592.24 2592.24 2590.30 1.000000
0 2592.23 2592.23 2589.64 1.000000
0 2592.23 2592.23 2590.83 1.000000
0 2592.23 2592.23 2590.49 1.000000
0 2592.23 2592.23 2590.78 1.000000
0 2592.23 2592.23 2590.84 1.000000
0 2592.22 2592.22 2588.80 1.000000
В основном соотношение составляет от 1.000000 до 6 знаков после запятой.
Графически (при этом масштаб оси Y должен быть таким же, как и предыдущий график):
Теперь код просто запускает горячий цикл, и не должно быть инструкций hlt
или mwait
, конечно же, ничего, что означало бы изменение более 10%. Я не могу точно сказать, что такое "циклы останова TM", но я бы поспорил, что они "циклы остановки охлаждения", трюк, используемый для временного дросселирования ЦП при достижении максимальной температуры. Тем не менее, я посмотрел на встроенные термисторные показания, и я никогда не видел перелома процессора 60C, намного ниже 90C-100C, где термическое управление срабатывает (я думаю).
Любая идея, что это может быть? Существуют ли подразумеваемые "циклы остановки" для перехода между различными турбочастотами? Это определенно происходит, потому что ящик не тихий, и поэтому частота турбонаддува прыгает вверх и вниз по мере того, как запускаются другие ядра и перестают работать на фоне (максимальная частота турбонаддува напрямую зависит от количества активных ядер: на моем ящике - 3,5, 3,3, 3,2, 3,1 ГГц на 1, 2, 3 или 4 ядра соответственно).
1Фактически, на некоторое время я действительно получал точные результаты до двух знаков после запятой: 2591.97 MHz
- итерация после итерации. Затем что-то изменилось, и я не совсем уверен, что и есть небольшое изменение около 0,1% в результатах rdstc
. Одной из возможностей является постепенная регулировка часов, выполняемая подсистемой синхронизации Linux, чтобы привести локальное время, полученное кристаллом, в соответствии с установленным временем ntpd
. Возможно, это просто кристаллический дрейф - последний график показывает постоянное увеличение измеряемого периода rdtsc
каждую секунду.
2 Графики не соответствуют тем же запускам, что и значения, отображаемые в тексте, потому что я не собираюсь обновлять графики каждый раз, когда меняю формат вывода текста. Качественное поведение, по сути, одно и то же на каждом прогоне.