Почему этот цикл задержки начинает работать быстрее после нескольких итераций без сна?

Рассматривать:

#include <time.h>
#include <unistd.h>
#include <iostream>
using namespace std;

const int times = 1000;
const int N = 100000;

void run() {
  for (int j = 0; j < N; j++) {
  }
}

int main() {
  clock_t main_start = clock();
  for (int i = 0; i < times; i++) {
    clock_t start = clock();
    run();
    cout << "cost: " << (clock() - start) / 1000.0 << " ms." << endl;
    //usleep(1000);
  }
  cout << "total cost: " << (clock() - main_start) / 1000.0 << " ms." << endl;
}

Вот пример кода. В первых 26 итерациях цикла синхронизации run функция стоит около 0,4 мс, но затем стоимость снижается до 0,2 мс.

Когда usleep без комментариев, цикл задержки занимает 0,4 мс для всех запусков, никогда не ускоряется. Зачем?

Код скомпилирован с g++ -O0 (без оптимизации), поэтому цикл задержки не оптимизируется. Он работает на процессоре Intel® Core ™ ( i3-3220) с тактовой частотой 3,30 ГГц, с универсальной версией 3.13.0-32 Ubuntu 14.04.1 LTS (Trusty Tahr).

2 ответа

Решение

После 26 итераций Linux увеличивает процессор до максимальной тактовой частоты, так как ваш процесс использует свой полный временной интервал пару раз подряд.

Если бы вы проверили счетчики производительности вместо времени настенных часов, вы бы увидели, что такты ядра на цикл задержки остались постоянными, подтверждая, что это всего лишь эффект DVFS (который все современные процессоры используют для работы с большей энергией). эффективная частота и напряжение большую часть времени).

Если вы тестировали Skylake с поддержкой ядра для нового режима управления питанием (когда аппаратное обеспечение полностью контролирует тактовую частоту), увеличение скорости произойдет гораздо быстрее.

Если вы оставите его работать некоторое время на процессоре Intel с Turbo, вы, вероятно, увидите, что время на итерацию снова немного увеличится, когда тепловые пределы потребуют, чтобы тактовая частота снизилась до максимальной постоянной частоты.


Представляя usleep не позволяет регулятору частоты процессора Linux увеличивать тактовую частоту, потому что процесс не генерирует 100% нагрузки даже на минимальной частоте. (Т.е. эвристика ядра решает, что процессор работает достаточно быстро для рабочей нагрузки, которая на нем работает.)



комментарии к другим теориям:

Re: теория Дэвида, что потенциальный контекст переключаться с usleep может загрязнить кеши: в общем, это неплохая идея, но это не помогает объяснить этот код.

Загрязнение кешем / TLB совсем не важно для этого эксперимента. Внутри окна синхронизации нет ничего, что касалось бы памяти, кроме конца стека. Большую часть времени проводят в крошечном цикле (1 строка кэша инструкций), который касается только одного int стековой памяти. Любое потенциальное загрязнение кеша во время usleep это крошечная доля времени для этого кода (реальный код будет другим)!

Более подробно для x86:

Призыв к clock() сам по себе может пропустить кэш, но пропадание кэша при выборке кода задерживает измерение времени запуска, а не является частью того, что измеряется. Второй звонок clock() почти никогда не будет задерживаться, потому что он все еще должен быть горячим в кеше.

run функция может быть в другой строке кэша от main (так как gcc отмечает main как "холодный", поэтому он меньше оптимизируется и помещается с другими холодными функциями / данными). Мы можем ожидать одну или две ошибки в кэше команд. Они, вероятно, все еще на той же странице 4k, хотя, так main вызовет потенциальную ошибку TLB перед входом во временную область программы.

gcc -O0 скомпилирует код OP в нечто вроде этого (проводник компилятора Godbolt): сохранение счетчика цикла в памяти в стеке.

Пустой цикл сохраняет счетчик цикла в памяти стека, поэтому на типичном процессоре Intel x86 цикл выполняется на одной итерации за ~6 циклов на процессоре IvyBridge OP, благодаря задержке пересылки хранилища, которая является частью add с назначением памяти (чтение-изменение-запись). 100k iterations * 6 cycles/iteration составляет 600 тыс. циклов, что преобладает над вкладом не более пары пропусков кэша (~200 циклов каждый при пропаданиях выборки кода, которые препятствуют выдаче дальнейших инструкций до их разрешения).

Выполнение не по порядку и пересылка хранилища должны в основном скрывать потенциальную ошибку кэша при доступе к стеку (как часть call инструкция).

Даже если счетчик циклов был сохранен в регистре, 100 000 циклов - это много.

Вызов usleep может или не может привести к переключению контекста. Если это произойдет, это займет больше времени, чем если бы это не так.

Другие вопросы по тегам