对两个连续测量进行基准测试时不一致

时间:2019-06-18 07:55:51

标签: c++ linux performance caching performance-testing

我正在对一个函数进行基准测试,我发现某些迭代要比其他迭代慢。

经过一些测试,我尝试对两个连续的测量进行基准测试,但仍然得到一些奇怪的结果。

代码为on wandbox

对我来说重要的是:

using clock = std::chrono::steady_clock;
// ...
for (int i = 0; i < statSize; i++)
{
    auto t1 = clock::now();
    auto t2 = clock::now();
}

循环已优化,如我们在godbolt上看到的。

call std::chrono::_V2::steady_clock::now()
mov r12, rax
call std::chrono::_V2::steady_clock::now()

代码使用以下代码编译:

g++  bench.cpp  -Wall  -Wextra -std=c++11 -O3

Intel® Xeon® W-2195 Processor上的gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)

我是计算机上的唯一用户,并且尝试在没有优先级(nicechrt)的情况下运行,结果是相同的。

我经过1亿次迭代得到的结果是:

100 000 000 iterations

Y轴以纳秒为单位,它是直线的结果

std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()

这4行让我想到:没有缓存/ L1 / L2 / L3缓存未命中(即使“ L3缓存未命中”行似乎与L2行太近了)

我不确定为什么会出现缓存未命中,可能是结果存储的原因,但是它不在测量的代码中。

我试图用1500的循环运行10000次程序,因为此处理器的L1高速缓存为:

lscpu | grep L1 
L1d cache:             32K
L1i cache:             32K

并且1500*16 bits = 24 000 bits小于32K,因此不会出现缓存缺失。

结果:

10 000 time the program with a loop of 1500

我还有4条线(还有一些杂音)。

因此,如果确实有缓存丢失,我不知道为什么会发生。

我不知道这是否对您有用,但我跑步:

sudo perf stat -e cache-misses,L1-dcache-load-misses,L1-dcache-load  ./a.out 1000

值为1 000 / 10 000 / 100 000 / 1 000 000

我在所有L1-dcache命中率中占4.70%至4.30%,对我来说似乎很不错。

所以问题是:

  • 这些减速的原因是什么?
  • 当我没有固定时间进行No操作时,如何为函数定性?

Ps:如果我缺少有用的信息/标志,我不知道,请随时提出!


如何繁殖

  1. 代码:

    #include <iostream>
    #include <chrono>
    #include <vector>
    
    int main(int argc, char **argv)
    {
        int statSize = 1000;
        using clock = std::chrono::steady_clock;
        if (argc == 2)
        {
            statSize = std::atoi(argv[1]);
        }
    
        std::vector<uint16_t> temps;
        temps.reserve(statSize);
        for (int i = 0; i < statSize; i++)
        {
    
            auto t1 = clock::now();
    
            auto t2 = clock::now();
    
            temps.push_back(
                std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count());
        }
    
        for (auto t : temps)
            std::cout << (int)t << std::endl;
    
        return (0);
    }
    
  2. 内部版本:

    g++  bench.cpp  -Wall  -Wextra -std=c++11 -O3
    
  3. 生成输出(需要sudo):

    在这种情况下,我运行了10 000次程序。每次采取100项措施,而我删除的第一种措施总是慢5倍:

     for i in {1..10000} ; do sudo nice -n -17 ./a.out 100 | tail -n 99  >> fast_1_000_000_uint16_100 ; done
    
  4. 生成图形:

    cat fast_1_000_000_uint16_100 | gnuplot -p -e "plot '<cat'"
    
  5. 我在计算机上获得的结果:

enter image description here


祖兰的答案和所有评论之后的位置

current_clocksource设置在tsc上,并且在dmesg中看不到任何开关,使用的命令是:

dmesg -T | grep tsc

use this script删除超线程(HT) 然后

grep -c proc /proc/cpuinfo
=> 18

从最后一个结果中减去1,以获得最后一个可用的核:

=> 17

编辑/ etc / grub / default并在GRUB_CMDLINE_LINUX中添加isolcpus =(最后结果):

GRUB_CMDLINE_LINUX="isolcpus=17"

最终:

sudo update-grub
reboot 
// reexecute the script

现在我可以使用:

taskset -c 17 ./a.out XXXX

所以我运行了1万次,循环了100次迭代。

for i in {1..10000} ; do sudo /usr/bin/time -v taskset -c 17 ./a.out 100  > ./core17/run_$i 2>&1 ; done

检查是否有Involuntary context switches

grep -L "Involuntary context switches: 0" result/* | wc -l
=> 0 

没有,很好。让我们来画图:

for i in {1..10000} ; do cat ./core17/run_$i | head -n 99 >> ./no_switch_taskset ; done
cat no_switch_taskset | gnuplot -p -e "plot '<cat'"

结果:

22 fail (sore 1000 and more) in 1 000 000

我不理解仍有22个大于1000的度量(大多数值在20附近)。

下一步,待定

做部分:

sudo nice -n -17 perf record...

祖兰答案的

1 个答案:

答案 0 :(得分:3)

我无法用这些特殊的群集线来复制它,但是这里是一些常规信息。

可能的原因

正如评论中所讨论的,在正常的 idle 系统上,这只是尽力而为。你至少还有

  1. 计划滴答计时器

  2. 绑定到特定代码的内核任务

  3. 您的任务可能出于任意原因从一个核心迁移到另一个核心

您可以在某些进程中使用isolcpus and taskset to get exclusive cores来避免某些问题,但是我认为您无法真正摆脱所有内核任务。另外,使用nohz=full to disable the scheduling tick。您还应该禁用超线程以从硬件线程获得对内核的独占访问。

除了taskset(我绝对建议将其用于任何性能评估)之外,这些都是非常不寻常的措施。

测量而不是猜测

如果怀疑会发生什么,通常可以设置一个度量来确认或反驳该假设。 perf和跟踪点非常有用。例如,我们可以从计划活动和一些中断开始:

sudo nice -n -17 perf record -o perf.data -e sched:sched_switch -e irq:irq_handler_entry -e irq:softirq_entry ./a.out ...

perf script现在将告诉您每次出现的情况。要将其与慢迭代相关联,可以使用perf probe和稍作修改的基准:

void __attribute__((optimize("O0"))) record_slow(int64_t count)
{
    (void)count;
}

...

    auto count = std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
    if (count > 100) {
        record_slow(count);
    }
    temps.push_back(count);

并使用-g

进行编译
sudo perf probe -x ./a.out record_slow count

然后将-e probe_a:record_slow添加到对perf record的呼叫中。现在,如果幸运的话,您会发现一些关闭事件,例如:

 a.out 14888 [005] 51213.829062:    irq:softirq_entry: vec=1 [action=TIMER]
 a.out 14888 [005] 51213.829068:  probe_a:record_slow: (559354aec479) count=9029

请注意:虽然这些信息可能会解释您的某些观察结果,但您进入的世界充满了更多令人困惑的问题和怪异之处。另外,尽管perf的开销很低,但是您所测量的内容可能会有一些扰动。

我们基准测试是什么?

首先,您需要弄清实际测量的内容:执行std::chrono::steady_clock::now()的时间。这样做至少可以弄清楚该测量开销以及时钟的精度,实际上是一件好事。

这实际上是一个棘手的问题。下面带有clock_gettime的此函数的成本取决于当前的Clocksource 1 。如果是tsc,就可以了-hpet 要慢得多。在操作过程中,Linux可能会将 2 tsc悄悄地切换到hpet

如何获得稳定的结果?

有时,您可能需要极端隔离地进行基准测试,但通常即使对于非常低级的微体系结构基准测试也没有必要。相反,您可以使用统计效果:重复测量。使用适当的方法(均值,分位数),有时您可能希望排除异常值。

如果测量内核的时间不比计时器精度长很多,您将不得不重复内核并在外部进行测量以获得吞吐量而不是等待时间,这可能会有所不同。

是的-基准测试权非常复杂 ,您需要考虑很多方面,尤其是当您靠近硬件并且内核时间非常短时。幸运的是,这里有一些帮助,例如Google's benchmark library在进行正确的重复次数以及具有实验因素方面提供了很多帮助。

1 /sys/devices/system/clocksource/clocksource0/current_clocksource

2 实际上它在dmesg中是类似

  

clocksource:CPU上的计时看门狗:由于时延过大,将clocksource'tsc'标记为不稳定: