为什么clock_gettime(CLOCK_REALTIME,..)上的呼叫延迟有如此大的差异?

时间:2018-11-11 18:53:01

标签: linux performance optimization timing benchmarking

我正在尝试计时clock_gettime(CLOCK_REALTIME,...)的通话时间。 “过去”我曾经在循环的顶部调用过一次,因为这是一个相当昂贵的调用。但是现在,我希望通过vDSO和一些时钟改进,它可能不会这么慢。

我编写了一些测试代码,这些代码使用__rdtscp来计时对clock_gettime的重复调用(rdtscp调用绕过了一个称为clock_gettime的循环,并将结果加在一起,只是这样编译器就不会进行太多优化)。

如果我快速连续调用clock_gettime(),则时间长度从大约45,000个时钟周期降低到500个周期。我认为部分原因可能是第一次调用必须加载vDSO代码的原因(对我而言仍然不完整),但是如何花一些调用才能获得500个我根本无法解释,这种行为似乎不管我如何测试,它都是恒定不变的:

42467
1114
1077
496
455

但是,如果我在clock_gettime调用之间睡眠(一秒钟或十秒钟,没关系),它只会达到大约4.7k个周期的稳定状态:

这里有10秒钟的睡眠时间:

28293
1093
4729
4756
4736

这里1秒钟睡觉:

61578
855
4753
4741
5645
4753
4732

高速缓存的行为似乎无法描述这一点(在台式机系统上没有做任何事情)。我应该为调用clock_gettime安排多少预算?为什么通话越来越快?为什么睡一小段时间如此重要?

tl; dr 我想了解呼叫clock_gettime(CLOCK_REALTIME,...)所花费的时间,不了解为什么连续快速调用时它运行得更快,而不是两次调用之间运行一秒

更新:这是proc 0上的cpuinfo

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\n", (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\n", t, (y-x));
    }

    exit(0);
}

2 个答案:

答案 0 :(得分:3)

第一次调用clock_gettime时,页面错误发生在包含该功能指令的页面上。在我的系统上,这是一个软页面错误,需要花费数千个周期(最多10,000个周期)来处理。我的CPU运行在3.4GHz。我认为您的CPU运行频率要低得多,因此处理系统上的页面错误会花费更多时间。但是这里的要点是,您对第一个clock_gettime的调用将比随后的调用花费更多的时间。

您的代码所表现出的第二个主要影响是由于指令高速缓存未命中而造成的严重停顿。似乎您只在调用两个函数,即nowprintf,但是这些函数调用其他函数,并且它们都在L1指令高速缓存上竞争。总的来说,这取决于如何在物理地址空间中对齐所有这些功能。当睡眠时间为零秒时,由于指令高速缓存未命中而导致的停顿时间实际上相对较小(您可以使用ICACHE.IFETCH_STALL性能计数器对此进行测量)。但是,当睡眠时间大于零秒时,由于操作系统将安排其他一些线程在同一内核上运行并且该线程将使用不同的指令和数据,因此该停顿时间会大大增加。这就解释了为什么您睡觉时clock_gettime需要花费更多时间来执行。

现在有关第二次和以后的测量。来自问题:

42467
1114
1077
496
455

我在系统上观察到第二个度量不一定比以后的度量大。我相信您的系统上也是如此。实际上,当您睡眠10秒钟或1秒时,似乎就是这种情况。在外部循环中,两个函数nowprintf包含数千条动态指令,它们还访问L1数据高速缓存。您在第二次和以后的测量之间看到的差异是可重现的。因此它是函数本身固有的。请注意,rdtscp指令本身的执行时间可能会相差4个周期。另请参见this

实际上,当所需的精度最多为一百万个周期时,clock_gettime很有用。否则,可能会产生误导。

答案 1 :(得分:0)

我无法复制您的结果。即使睡眠时间较长(10秒),循环次数也很少(100次),我的时序总是少于100个时钟(在我的2.6 GHz系统上小于38 ns)。

例如:

./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的{​​{1}}。

第二个最可能的原因是clock_gettime没有通过系统上的VDSO,即使最终使用了clock_gettime(CLOCK_REALTIME),系统调用也是如此。我想这可能是由于旧的libc版本或类似原因造成的。

第三个最可能的原因是系统上的rdtsc速度很慢,可能是因为它在系统上已被虚拟化或禁用,并且是通过VM出口或OS陷阱实现的。

单循环结果

尝试每个循环进行一次rdtsc调用,经过前几次试验后,我仍然获得“快速”结果。例如,clock_gettime给出:

./clockt 0 20 1

请注意,我对测试程序进行了一次修改,以打印每次调用的时间,这似乎比总时间有用。 init run 15932 trial 0 took 352 (352 cycles per call) trial 1 took 196 (196 cycles per call) trial 2 took 104 (104 cycles per call) trial 3 took 104 (104 cycles per call) trial 4 took 104 (104 cycles per call) trial 5 took 104 (104 cycles per call) trial 6 took 102 (102 cycles per call) trial 7 took 104 (104 cycles per call) ... 行已修改为:

printf