Почему задержка вызова в clock_gettime (CLOCK_REALTIME, ..) меняется так сильно?

Я пытаюсь clock_gettime(CLOCK_REALTIME,...) как длится clock_gettime(CLOCK_REALTIME,...) для вызова. «В тот же день я называл это когда-то на вершине цикла, потому что это был довольно дорогой звонок. Но теперь я надеялся, что с vDSO и некоторыми улучшениями часов это может быть не так медленно.

Я написал некоторый тестовый код, который использовал __rdtscp для повторных вызовов clock_gettime (вызовы rdtscp обошли цикл, который вызвал clock_gettime и добавил результаты вместе, просто чтобы компилятор не слишком оптимизировал).

Если я вызову clock_gettime() в быстрой последовательности, время будет составлять около 45k тактовых циклов до 500 циклов. Некоторые из этого, я думал, могут быть внесены в первый вызов, который должен загрузить код vDSO (все еще не полный смысл для меня), но как это требует нескольких звонков, чтобы получить 500, я не могу вообще объяснить, и это поведение кажется быть постоянным, независимо от того, как я его тестирую:

42467
1114
1077
496
455

Однако, если я сплю (на секунду или десять, не имеет значения) между вызовами clock_gettime, он достигнет устойчивого состояния около 4,7 тыс. Циклов:

Здесь в 10 секунд спит:

28293
1093
4729
4756
4736

Здесь в 1 секунду спит:

61578
855
4753
4741
5645
4753
4732

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

tl; dr Я пытаюсь понять время, которое требуется для вызова clock_gettime(CLOCK_REALTIME,...) , не понимает, почему он работает быстрее при вызове в быстрой последовательности, а не в clock_gettime(CLOCK_REALTIME,...) между вызовами.

Обновление: вот cpuinfo для proc 0

processor   : 0
vendor_id   : GenuineIntel
cpu family  : 6
model       : 158
model name  : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
stepping    : 9
microcode   : 0x84
cpu MHz     : 2800.000
cache size  : 6144 KB
physical id : 0
siblings    : 8
core id     : 0
cpu cores   : 4
apicid      : 0
initial apicid  : 0
fpu     : yes
fpu_exception   : yes
cpuid level : 22
wp      : yes
flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb intel_pt tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp
bugs        :
bogomips    : 5616.00
clflush size    : 64
cache_alignment : 64
address sizes   : 39 bits physical, 48 bits virtual
power management:

Вот обновленный тестовый код:

#include <time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <x86intrin.h>

// compiled gcc -Wall -O3 -o clockt clockt.cpp
// called glockt sleeptime trials loops

unsigned long long now() {
    struct timespec s;
    clock_gettime(CLOCK_REALTIME, &s);
    return (s.tv_sec * 1000000000ull) + s.tv_nsec;
}

int main(int argc, char **argv) {
    int sleeptime = atoi(argv[1]);
    int trials = atoi(argv[2]);
    int loops = atoi(argv[3]);

    unsigned long long x, y, n = 0;
    unsigned int d;


    x = __rdtscp(&d);
    n = now();
    asm volatile("": "+r" (n));
    y = __rdtscp(&d);

    printf("init run %lld
", (y-x));

    for(int t = 0; t < trials; ++t) {
        if(sleeptime > 0) sleep(sleeptime);
        x = __rdtscp(&d);
        for(int l = 0; l < loops; ++l) {
            n = now();
            asm volatile("": "+r" (n));
        }
        y = __rdtscp(&d);
        printf("trial %d took %lld
", t, (y-x));
    }

    exit(0);
}

Всего 2 ответа


clock_gettime самое первое время clock_gettime , на странице появляется страница, которая содержит инструкции этой функции. В моей системе это неправильная ошибка страницы, и требуется несколько тысяч циклов (до 10 000 циклов). Мой процессор работает на частоте 3,4 ГГц. Я думаю, что ваш процессор работает на гораздо более низкой частоте, поэтому обработка ошибки страницы в вашей системе займет больше времени. Но дело здесь в том, что первый вызов clock_gettime займет гораздо больше времени, чем последующие вызовы, что и есть то, что вы наблюдаете.

Вторым существенным эффектом, который проявляет ваш код, является значительное количество ларьков из-за промахов кэша команд. Может показаться, что вы вызываете только две функции, а именно now и printf , но эти функции вызывают другие функции, и все они конкурируют в кеше команд L1. В целом, это зависит от того, как все эти функции выровнены в физическом адресном пространстве. Когда время сна равно нулю, время останова из-за промахов кэша команд на самом деле относительно невелико (вы можете измерить это с помощью ICACHE.IFETCH_STALL производительности ICACHE.IFETCH_STALL ). Однако, когда время сна больше нуля секунд, это время сваливания становится значительно больше, потому что ОС будет планировать запуск другого потока на одном ядре, и этот поток будет иметь разные инструкции и данные. Это объясняет, почему, когда вы спите, clock_gettime занимает больше времени для выполнения.

Теперь о втором и последующих измерениях. Из вопроса:

42467
1114
1077
496
455

Я наблюдал в своей системе, что второе измерение не обязательно больше, чем последующие измерения. Я считаю, что это также верно в вашей системе. На самом деле это похоже на то, когда вы спите 10 секунд или 1 секунду. Во внешнем цикле две функции now и printf содержат тысячи динамических инструкций, а также доступ к кэшу данных L1. Изменчивость, которую вы видите между вторым и последующим измерениями, воспроизводима. Так что это присуще самим функциям. Обратите внимание, что время выполнения команды rdtscp может варьироваться в 4 цикла. См. Также это .

На практике время clock_gettime полезно, когда желаемая точность составляет не более миллиона циклов. В противном случае это может ввести в заблуждение.


Я не смог воспроизвести ваши результаты. Даже при большом времени сна (10 секунд) и небольшом числе петель (100) я всегда получаю синхронизацию менее 100 тактов (менее 38 нс на моей системе с частотой 2,6 ГГц).

Например:

./clockt 10 10 100
init run 14896
trial 0 took 8870 (88 cycles per call)
trial 1 took 8316 (83 cycles per call)
trial 2 took 8384 (83 cycles per call)
trial 3 took 8796 (87 cycles per call)
trial 4 took 9424 (94 cycles per call)
trial 5 took 9054 (90 cycles per call)
trial 6 took 8394 (83 cycles per call)
trial 7 took 8346 (83 cycles per call)
trial 8 took 8868 (88 cycles per call)
trial 9 took 8930 (89 cycles per call)

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

Вторая, скорее всего, причина в том, что clock_gettime(CLOCK_REALTIME) не проходит через VDSO в вашей системе, так что это системный вызов, даже если rdtsc в конечном счете используется. Думаю, это может быть связано с старой версией libc или чем-то вроде этого.

Третья, по всей вероятности, причина в том, что rdtsc в вашей системе медленный, возможно, потому, что он виртуализирован или отключен в вашей системе и реализуется через выход VM или ловушку ОС.


Есть идеи?

10000