clock_gettime выполняется дольше, когда программа запускается из терминала

Я пытался измерить время для фрагмента кода и заметил, что время было примерно на 50 нс быстрее, когда я запускал программу из своего редактора QtCreator, по сравнению с тем, когда я запускал ее из оболочки bash, запущенной в gnome-терминал. Я использую Ubuntu 20.04 в качестве ОС.

Небольшая программа для воспроизведения моей проблемы:

#include <stdio.h>
#include <time.h>

struct timespec now() {
  struct timespec now;
  clock_gettime(CLOCK_MONOTONIC, &now);
  return now;
}

long interval_ns(struct timespec tick, struct timespec tock) {
  return (tock.tv_sec - tick.tv_sec) * 1000000000L
      + (tock.tv_nsec - tick.tv_nsec);
}

int main() {
    // sleep(1);
    for (size_t i = 0; i < 10; i++) {
        struct timespec tick = now();
        struct timespec tock = now();
        long elapsed = interval_ns(tick, tock);
        printf("It took %lu ns\n", elapsed);
    }
    return 0;
}

Вывод при запуске из QtCreator

It took 84 ns
It took 20 ns
It took 20 ns
It took 21 ns
It took 21 ns
It took 21 ns
It took 22 ns
It took 21 ns
It took 20 ns
It took 21 ns

И при запуске из моей оболочки внутри терминала:

$ ./foo 
It took 407 ns
It took 136 ns
It took 74 ns
It took 73 ns
It took 77 ns
It took 79 ns
It took 74 ns
It took 81 ns
It took 74 ns
It took 78 ns

Вещи, которые я пробовал, которые не имели значения

  • Разрешение QtCreator запускать программу в терминале
  • Использование вызовов rdtsc и rdtscp вместо clock_gettime (те же относительные различия во время выполнения)
  • очистка окружения из терминала, запустив его под env -i
  • Запуск программы с использованием sh вместо bash

Я проверил, что один и тот же двоичный файл вызывается во всех случаях. Я убедился, что значение nice равно 0 для программы во всех случаях.

Вопрос

Почему запуск программы из моей оболочки имеет значение? Любые предложения о том, что попробовать?

Обновлять

  • Если я добавлю вызов sleep(1) в начало main, вызовы QtCreator и gnome-terminal/bash сообщат о более длительном времени выполнения.

  • Если я добавлю вызов system(ps -H) в начале main, но уберу ранее упомянутый sleep(1): оба вызова сообщают о коротком времени выполнения (~ 20 нс).


person Daniel Näslund    schedule 03.08.2020    source источник
comment
В стороне: когда long является 32-битным, код легко переполняется. Предложить long long interval_ns(struct timespec tick, struct timespec tock) { return (tock.tv_sec - tick.tv_sec) * 1000000000LL + (tock.tv_nsec - tick.tv_nsec); } (изменить тип и LL)   -  person chux - Reinstate Monica    schedule 04.08.2020


Ответы (1)


Просто добавьте больше итераций, чтобы дать ЦП время для разгона до максимальной тактовой частоты. Ваши медленные времена связаны с тактовой частотой ЦП с низким энергопотреблением в режиме ожидания.

QtCreator, по-видимому, использует достаточно процессорного времени, чтобы это произошло до запуска вашей программы, иначе вы компилируете + запускаете, а процесс компиляции служит разминкой. (по сравнению с fork/execve bash, имеющим меньший вес.)

См. раздел Идиоматический способ оценки производительности?, чтобы узнать больше о прогревочных прогонах при бенчмаркинге. а также Почему этот цикл задержки начинает работать быстрее после нескольких итераций без сна?

На моем i7-6700k (Skylake) под управлением Linux увеличения количества итераций цикла до 1000 достаточно, чтобы последние итерации работали на полной тактовой частоте, даже после того, как первые две итерации обрабатывают ошибки страниц, прогревают iTLB, кэш uop, данные. кеш и так далее.

$ ./a.out      
It took 244 ns
It took 150 ns
It took 73 ns
It took 76 ns
It took 75 ns
It took 71 ns
It took 72 ns
It took 72 ns
It took 69 ns
It took 75 ns
...
It took 74 ns
It took 68 ns
It took 69 ns
It took 72 ns
It took 72 ns        # 382 "slow" iterations in this test run (copy/paste into wc to check)
It took 15 ns
It took 15 ns
It took 15 ns
It took 15 ns
It took 16 ns
It took 16 ns
It took 15 ns
It took 15 ns
It took 15 ns
It took 15 ns
It took 14 ns
It took 16 ns
...

В моей системе для параметра energy_performance_preference установлено значение balance_performance, поэтому аппаратный регулятор P-состояния не так агрессивен, как с performance. Используйте grep . /sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference для проверки, используйте sudo для изменения:

sudo sh -c 'for i in /sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference;do echo balance_performance > "$i";done'

Однако даже запуска под perf stat ./a.out достаточно, чтобы очень быстро разогнаться до максимальной тактовой частоты; это действительно не займет много времени. Но синтаксический анализ команды bash после нажатия клавиши возврата очень дешев, не так много работы ЦП, прежде чем он вызовет execve и достигнет main в вашем новом процессе.

Кстати, printf с линейным буфером вывода занимает большую часть процессорного времени в вашей программе. Вот почему требуется так мало итераций, чтобы набрать скорость. например если вы запустите perf stat --all-user -r10 ./a.out, вы увидите, что тактовая частота ядра пользовательского пространства в секунду составляет всего около 0,4 ГГц, остальное время ядро ​​​​тратит на write системные вызовы.

person Peter Cordes    schedule 03.08.2020
comment
Спасибо за отличный ответ. Я добавил тег x86 в надежде призвать вас. :-) Я думал, что более длительное время для первых 2-3 итераций представляет собой время прогрева. Например. что после этого процессор разогнался до полной тактовой частоты. Видимо нужно больше. Обидно, что я не подумал о смене регулятора производительности, но, с другой стороны, я узнал что-то новое, а это всегда дорогого стоит. - person Daniel Näslund; 03.08.2020
comment
@DanielNäslund: Первые несколько итераций — это другие виды прогрева, отличные от тактовой частоты, например ошибки страницы. Если бы у вас было что-то еще, работающее на другом ядре, поддерживающее высокую тактовую частоту до того, как это запустилось (например, простой бесконечный цикл), вы бы увидели, что эти первые пару интервалов будут короче. Или если вы сделали perf stat -r10 ./a.out, чтобы запустить его 10 раз подряд. Помимо изменения регулятора, вы можете сделать множество вещей, тем более что клиентские чипы Intel (не серверные) работают на всех своих ядрах с одинаковой тактовой частотой, поэтому одно ядро ​​на максимуме означает, что другие ядра на максимуме. - person Peter Cordes; 03.08.2020