Why do I see 400x outlier timings when calling clock_gettime repeatedly?

时间:2018-03-09 19:20:37

标签: c++ linux performance x86 clock

I'm trying to measure execution time of some commands in c++ by using the physical clock, but I have run into a problem that the process of reading off the measurement from the physical clock on the computer can take a long time. Here is the code:

#include <string>
#include <cstdlib>
#include <iostream>
#include <math.h>
#include <time.h>

int main()
{
      int64_t mtime, mtime2, m_TSsum, m_TSssum, m_TSnum, m_TSmax;
      struct timespec t0;
      struct timespec t1;
      int i,j;
      for(j=0;j<10;j++){
      m_TSnum=0;m_TSsum=0; m_TSssum=0; m_TSmax=0;
      for( i=0; i<10000000; i++) {
            clock_gettime(CLOCK_REALTIME,&t0);
            clock_gettime(CLOCK_REALTIME,&t1);
            mtime = (t0.tv_sec * 1000000000LL + t0.tv_nsec);
            mtime2= (t1.tv_sec * 1000000000LL + t1.tv_nsec);

            m_TSsum += (mtime2-mtime);
            m_TSssum += (mtime2-mtime)*(mtime2-mtime);
            if( (mtime2-mtime)> m_TSmax ) { m_TSmax = (mtime2-mtime);}
            m_TSnum++;
      }
      std::cout << "Average "<< (double)(m_TSsum)/m_TSnum
            << " +/- " << floor(sqrt( (m_TSssum/m_TSnum  - ( m_TSsum/m_TSnum ) *( m_TSsum/m_TSnum ) ) ) )
            << " ("<< m_TSmax <<")" <<std::endl;
      }
}

Next I run it on a dedicated core (or so the sysadmin tells me), to avoid any issues with process being moved to background by scheduler:

$ taskset -c 20 ./a.out

and this is the result I get:

Average 18.0864 +/- 10 (17821)
Average 18.0807 +/- 8 (9116)
Average 18.0802 +/- 8 (8107)
Average 18.078 +/- 6 (7135)
Average 18.0834 +/- 9 (21240)
Average 18.0827 +/- 8 (7900)
Average 18.0822 +/- 8 (9079)
Average 18.086 +/- 8 (8840)
Average 18.0771 +/- 6 (5992)
Average 18.0894 +/- 10 (15625)

So clearly it takes about 18 nanosecond (on this particular server) to call clock_gettime(), but what I can't understand why the "max" time seems to be between 300 and 1000 times longer?

If we assume that the core is truly dedicated to this process and not used by something else (which may or may not be true; when not running on dedicated core, the average time is the same, but the sd/max are somewhat bigger), what else could cause these "slowdowns" (for the lack of a better name)?

3 个答案:

答案 0 :(得分:8)

为什么选择异常值?

当您在两次clock_gettime次呼叫中迭代1000万次时,有许多软件和硬件相关的原因可能会导致您看到异常事件(以及非异常值变化)。这些原因包括:

  • 上下文切换:调度程序可能决定在CPU之间迁移您的进程,即使您将进程固定到CPU,操作系统也可能会定期在您的逻辑CPU上运行其他。< / LI>
  • SMT:假设这是在带有SMT的CPU上(例如,在x86上超线程),调度程序可能会定期在兄弟核心上安排一些事情(与您的进程相同的物理核心)。这可能会极大地影响代码的整体性能,因为两个线程正在竞争相同的核心资源。此外,SMT和非SMT执行之间可能存在过渡期,其中没有任何执行,因为当SMT执行开始时核心必须重新占用一些资源。
  • 中断:典型系统将至少每秒接收数百个中断,包括网卡,图形设备,硬件时钟,系统定时器,音频设备,IO设备,跨CPU IPI等。尝试watch -n1 cat /proc/interrupts,看看您可能认为是一个空闲系统的动作是如何发生的。
  • 硬件暂停:CPU本身可能会因各种原因(例如电源或热量限制)或仅仅因为CPU is undergoing a frequency transition而定期停止执行指令。
  • System Management Mode:除了操作系统看到和处理的中断之外,x86 CPU还有一种&#34;隐藏中断&#34;它允许在CPU上执行SMM功能,唯一明显的影响是用于测量实时的周期计数器中的周期性意外跳转。
  • 正常的性能变化:您的代码每次都以完全相同的方式执行。初始迭代将遭受数据和指令缓存未命中,并且对于诸如分支方向之类的事情具有未经训练的预测因子。即使处于明显的稳定状态&#34;你可能仍然会受到你无法控制的事情的影响。
  • 不同的代码路径:你可能希望你的循环每次通过 1 执行完全相同的指令:毕竟,没有什么是真正改变的,对吧?好吧,如果你深入研究clock_gettime的内部结构,你很可能会发现一些分支在发生一些溢出时会采取不同的路径,或者在更新时从VDSO比赛中的调整因子中读取等等。 / LI>

这甚至都不是一个全面的列表,但至少应该让你尝试一些可能导致异常值的因素。您可以消除或减少某些的影响,但在x86上的现代非实时 2 操作系统中通常无法完全控制。

我的猜测

如果我不得不猜测,基于典型的异常值~8000 ns,这对于上下文切换中断可能太小,您可能会看到处理器频率缩放的影响改变TurboBoost比率。这是一个满口的,但基本上是现代的x86芯片运行在不同的&#34; max turbo&#34;速度取决于活动的核心数量。例如,如果一个核心处于活动状态,我的i7-6700HQ将以3.5 GHz运行,如果分别激活2,3或4个核心,则仅运行3.3,3.2或3.1 GHz。

这意味着即使您的进程永不中断,任何在其他CPU上运行的工作都可能会导致频率转换(例如,因为您从1个活动核心转换为2个活动核心)并且在这种转换期间,CPU在电压稳定的同时空闲数千个周期。您可以找到一些详细的数字和测试in this answer,但结果是在测试的CPU上稳定大约需要20,000个周期,非常符合您观察到的~8000纳秒的异常值。有时你可能会在一段时间内获得两次过渡,使影响加倍,等等。

缩小范围

获取分发

如果您仍想知道异常值的原因,可以采取以下步骤并观察对异常值行为的影响。

首先,您应该收集更多数据。您应该收集具有合理铲斗尺寸的直方图(例如100 ns,甚至更好的某种类型的几何铲斗尺寸,以便在更短的时间内提供更高的分辨率),而不是仅重新编码超过10,000,000次迭代。这将是一个巨大的帮助,因为您将能够准确地看到时间聚集的位置:完全有可能除了您注意到的6000 - 17000 ns异常值之外还有其他效果&#34; max&# 34;,他们可能有不同的原因。

直方图还可以让您了解离群值频率,您可以将其与可以测量的事物的频率相关联,以查看它们是否匹配。

现在添加直方图代码也可能为定时循环增加更多差异,因为(例如)您将根据时间值访问不同的缓存行,但这是可管理的,尤其是因为记录时间发生在&#34;定时区域之外&#34;。

问题特定缓解

有了这些,您可以尝试系统地检查我上面提到的问题,看看它们是否是原因。以下是一些想法:

  1. 超线程:在运行单线程基准测试时,只需在BIOS中将其关闭,即可一举消除所有类问题。总的来说,我发现这也会导致细粒度基准差异的大幅减少,因此这是一个良好的开端。
  2. 频率调整:在Linux上,您通常可以通过将性能调控器设置为&#34; performance&#34;来禁用子标称频率调整。如果您正在使用/sys/devices/system/cpu/intel_pstate/no_turbo驱动程序,则可以通过将0设置为intel_pstate来禁用超名义(也称为turbo)。如果您有其他驱动程序,也可以操作turbo模式directly via MSR,如果其他所有驱动程序都失败,您也可以在BIOS中执行此操作。在linked question中,当turbo被禁用时,异常值基本消失,所以首先要尝试一下。

    假设您确实希望在生产中继续使用turbo,您可以手动将最大turbo比限制为适用于N个核心的某个值(例如,2个核心),然后使其他CPU脱机,以便最多该数量的核心将永远活跃。然后,无论有多少核心处于活动状态,您都可以始终以新的最大涡轮增压运行(当然,在某些情况下,您可能仍会受到功率,电流或热量限制)。

  3. 中断:你可以搜索&#34;中断亲和力&#34;尝试将中断移入/固定固定核心,并查看对异常值分布的影响。您还可以计算中断的数量(例如,通过/proc/interrupts)并查看计数足以解释异常值。如果你发现特定的计时器中断是原因,你可以探索各种&#34; tickless&#34; (又名&#34; NOHZ&#34;)您的内核提供的模式可以减少或消除它们。您也可以通过x86上的HW_INTERRUPTS.RECEIVED性能计数器直接计算它们。
  4. 上下文切换:您可以使用实时优先级或isolcpus来阻止其他进程在您的CPU上运行。请记住,上下文切换问题虽然通常被定位为主要/唯一问题,但实际上相当罕见:最多它们通常以HZ速率发生(现代内核通常为250 /秒) - 但它将是在大多数空闲系统上很少见,调度程序实际上会决定在繁忙的CPU上调度另一个进程。如果你使基准测试循环变短,通常几乎可以完全避免上下文切换。
  5. 与代码相关的性能变化:您可以使用perf等各种分析工具检查是否发生这种情况。您可以仔细设计数据包处理代码的核心,以避免诸如缓存未命中之类的异常事件,例如通过预先触摸缓存行,并且您可以尽可能避免使用具有未知复杂性的系统调用。
  6. 虽然上述某些内容纯粹是出于调查目的,但其中许多内容都可以帮助您确定造成暂停的原因并减轻它们。

    我并不知道所有问题的缓解 - 例如SMM,你可能需要专门的硬件或BIOS才能避免。

    1 好吧,除非在触发if( (mtime2-mtime)> m_TSmax )条件的情况下 - 但这应该是罕见的(也许你的编译器已经使它成为无分支,在这种情况下有只有一条执行路径。)

    2 实际上并不清楚你可以得到零差异&#34;即使使用硬实时操作系统:某些特定于x86的因素(如SMM模式和DVFS相关档位)似乎也是不可避免的。

答案 1 :(得分:3)

taskset命令定义了您的进程的亲和性,这意味着您的进程被限制为在指定的CPU核心上运行。它不会以任何方式限制其他进程,这意味着它们中的任何进程都可以随时抢占您的进程(因为所有进程都允许在您为进程选择的CPU核心上运行)。因此,您的最大读取间隔时间(那些5-25 usec)可能代表CPU上的其他进程或中断运行时间以及上下文切换时间。除此之外,您还可以使用CLOCK_REALTIME进行NTP更正等。要测量时间间隔,您应该使用CLOCK_MONOTONIC(或特定于Linux的CLOCK_MONOTONIC_RAW)。

答案 2 :(得分:-2)

这在现代c ++中更容易

#include <chrono>
auto start = std::chrono::steady_clock::now();
.....
auto stop = std::chrono::steady_clock::now();
auto duration = stop - start;

对于非实时操作系统来说,18纳秒非常快。你真的需要比这更准确地测量一些东西吗?根据我的计算,18ns在4GHz CPU上只有72个时钟周期。