测量长时间运行的代码

发布时间 2024-01-09 01:10:11作者: deeplearnMs

如果程序只是运行一个计算密集型的任务,那么分析器会自动地告诉我们程序中的热点在
哪里。不过如果程序要做许多不同的处理,可能在分析器看来,没有任何一个函数是热
点。程序还有可能会花费大量的时间等待 I/O 或是外部事件,这样降低了程序的性能,增
加了程序的实际运行时间。在这种情况下,我们需要测量程序中各个部分的时间,然后试
着减少其中低效部分的运行时间。

开发人员通过不断地缩小长时间运行的任务的范围直至定位其中一段代码花费了太长时
间,感觉不对劲这种方式来查找代码中的热点。在找出这些可疑代码后,开发人员会在测
试套件中对小的子系统或是独立的函数进行优化实验。
测量运行时间是一种测试关于“如何减少某个特定函数的性能开销”的假设的有效方式。
一般,我们很难意识到可以通过编程在计算机上实现秒表功能。你可以非常方便地使用手
机或是手提电脑在工作日的 645 叫醒你,或是在早上 10 点的站立会议前 5 分钟提醒你
参加会议。但是在现代计算机上测量亚微秒级的运行时间却是有点难度的,特别是因为在
普通的 Window/PC 平台上存在没有可以稳定地工作于不同型号的硬件和不同的软件版本
上的高精度计时器的历史遗留问题。
因此,作为一名开发人员,你需要随时准备好制作一个自己的秒表,而且必须知道它们以
后可能会发生变化。为了使这成为可能,接下来我会讨论如何测量时间以及有哪些工具可
用于在计算机上测量时间。

一点关于测量时间的知识

一次完美的测量是指精确地得到大小、重量或者在本内容中是某个事件每次持续的时间。完
美的测量就像是将弓箭不断地精准地射中靶心一样。这种箭术只存在于故事书中,测量也
是一样的

真正的测量实验(就像真正的弓箭)必须能够应对可变性variation:可能破坏完美测
量的误差源。可变性有两种类型:随机的和系统的。随机的可变性对每次测量的影响都不
同,就像一阵风导致弓箭偏离飞行线路一样。系统的可变性对每次测量的影响是相似的,
就像一位弓箭手的姿势会影响他每一次射箭都偏向靶子的左边一样。
可变性自身也是可以测量的。衡量一次测量过程中的可变性的属性被称为精确性precision
正确性trueness。这两种属性组合成的直观特性称为准确性accuracy

 

1. 精确性正确性和准确性
很明显,对测量感到兴奋的科学家就相关的专业用语展开了喋喋不休的争论。你只需在维
基百科上查找一下“准确性”这个词,就会发现关于究竟应该使用哪些词来解释已经达成
一致的概念有多少争议了。我选择使用 1994 版的 ISO 5725-1 中的上下文来解释术语:“测
量方法和结果中的准确性(正确性和精确性)——卷 1:通用原则和定义”1994

如果测量不受随机可变性的影响,它就是精确的。也就是说,如果反复地测量同一现象,
而且这些测量值之间非常接近,那么测量就是精确的。一系列精确的测量中可能仍然包含
系统的可变性。

如果测量一个事件(比如一个函数的运行时间)10 次,而且 10 次的结果完全相同,我们
可以认为测量是精确的。(像在任何实验中一样,我应当会对此持怀疑态度,直到找到足
够的证据为止。)如果其中只有 6 次结果相同,3 次结果略微有些不同,1 次结果的差异非
常大,那么测量就是不够精确的。
如果测量不受系统可变性的影响,它就是正确的。也就是说,如果反复地测量同一现象,
而且所有测量结果的平均值接近实际值,那可以认为测量是正确的。每次独立的测量可能
受到随机可变性的影响,所以测量结果可能会更接近或是偏离实际值。

 

测量的准确性是一个取决于每次独立的测量结果与实际值有多接近的非正式的概念。与实
际值的差异由随机可变性与系统可变性两部分组成。只有同时具有精确性和正确性的测量
才是准确的测量。

2. 测量时间
本书中涉及的软件性能测量要么是测量持续时间(两个事件之间的时间),要么是测量
(单位时间内事件的数量,与持续时间相对)。用于测量持续时间的工具是时钟
所有时钟的工作原理都是周期性地计数。某些时钟的计数会表示为时、分、秒,有些则是
直接显示时标的次数。但是时钟(除了日晷外)是并不会直接测量时、分、秒的。它们只
会对时标进行计数,然后只有将时标计数值与秒基准的时钟进行比较后才能校准时钟,显
示出时、分、秒。
周期性地改变的东西受到可变性的影响也会出现误差。有些可变性是随机的,有些可变性
则是系统的。

日晷利用了地球的周期性旋转。从定义上说,一次完整的旋转是一天。地球并非完美的
时钟,不仅是因为周期太长,而且我们发现由于大陆在它表面上缓慢地移动,它的旋转
速度时快时慢(微秒级别)。这种可变性是随机的;来自月球和太阳的潮汐力会降低地
球的整体旋转速率。这种可变性是系统的。
老式时钟会对钟摆有规律的摆动计数。齿轮会随着钟摆驱动指针旋转来显示时间。钟摆
摆动的间隔可以手动调整,这样所显示的时间可以与地球旋转同步。钟摆摆动的周期取
决于钟摆的重量和它的长度,这样就可以根据需要让摆动得更快或是更慢。这种可变性
是系统的;而即使在最开始钟摆的摆动非常精准,但摩擦、气压和累积的灰尘都会对摆
动造成影响。这些都是随机可变性因素。
电子时钟使用它的交流电源的周期性的 60Hz 正弦波驱动同步电机。齿轮会下分基本振
荡和驱动指针来显示时间。电子时钟也并非完美的时钟,因为根据惯例(不是自然法
则)交流电源的周期只有 60Hz(在美国)。当负荷过高时,电力公司会先降低振荡周期,

稍后又提高振荡周期,这样电子时钟并不会走慢。所以,在炎热夏日的午后电子时钟的
一秒可能会比凉爽夜晚的一秒快(虽然我们总是对此表示怀疑)。这种可变性是随机的。
将一个为美国用户制造的电子时钟插入到欧洲 50Hz 的交流电源插座中,它会走得慢。
与气温引起的随机可变性相比,这种由欧洲电源插座引起的可变性是系统的。
数字腕表采用石英晶体的诱导振动作为基本振动。逻辑电路会下分基本振动并驱动时间
显示。石英晶体的振动周期取决于它的大小、温度以及加载的电压。石英晶体的大小的
影响是系统的可变性,而温度和电压的可变性则是随机的。
时标计数值肯定是一个无符号的值。不可能存在 -5 次时标。我之所以在这里提醒大家这
个看似非常明显的事实,是因为正如稍后会向大家展示的,许多开发人员实现计时函数时
选择有符号类型来表示持续时间。我不知道为什么他们这么做。我那十几岁的儿子应该会
说:“这没什么大不了。

 

3. 测量分辨率
测量的分辨率是指测量所呈现出的单位的大小。

时间测量的有效分辨率会受到潜在波动的持续时间的限制。时间测量结果可以是一次或者
两次时标,但不能是这两者之间。这些时标之间的间隔就是时钟的有效分辨率。
观察人员可能会察觉到一个走得很慢的时钟的两次时标之间发生的事情,例如钟摆的一次
摆动。这只是说明在人类脑海中有一个更快的时钟(虽然没有那么准确),他们会将这个
时钟的时间与钟摆的时间进行比较。观察人员如果想测量那些不可感知的持续时间,例如
毫秒级别,只能用时钟的时标。

 

在测量的准确性与它的分辨率之间是没有任何必需的关联的。例如,假设我记录了我每天
的工作,那么我可以报告说我花了两天来编写本节内容。在这个例子中,测量的有效分辨
率是“天”。如果我想把这个时间换成秒,那么可以报告说成我花了 172 800 秒来编写本节
内容。但除非我手头上有一个秒表,否则以秒为单位进行报告会让人误认为比之前更加准
确,或是给人一种没有吃饭和睡觉的错觉。
测量结果的单位可能会比有效分辨率小,因为单位才是标准。我有一个可以以华氏温度为
单位显示温度的烤箱。恒温器控制着烤箱,但是有效分辨率只有 5°F。所以在烤箱加热的
过程中,显示屏上显示的温度会是 300°F,接着是 305°F310°F315°F 等。以一度为单
位显示温度应该比恒温器的单位更合理。有效分辨率只有 5°F 只是表示测量的最低有效位
只能是 0 或者 5
当读者知道他们身边廉价的温度计、尺子和其他测量设备的有效分辨率后可能会感到吃惊
和失望,因为这些设备的显示分辨率是 1 个单位或是 1/10 单位。

用多个时钟测量

当两个事件在同一个地点发生时,很容易通过一个时钟的时标计数来测量事件的经过时
间。但是如果这两个事件发生在相距很远的不同地点,可能就需要两个时钟来测量时间。
而两个不同时钟的时标次数无法直接比较。

 

人类想到了一个办法,那就是通过与国际协调时间(Coordinated Universal Time)同步。
国际协调时间与经度 0 度的天文学上的午夜同步,而经度 0 度这条线穿过了英格兰格林威
治皇家天文台中的一块漂亮的牌匾(请参见图 3-5。这样就可以将一个以时标计数值表示
的时间转换为以时分秒表示的相对 UTCUniversal Time Coordinated,国际协调时间,
法国和英国的时钟专家商定的一个既不是法式拼写也不是英式拼写的缩写)午夜的时间。

测量性能 35
如果两个时钟都与 UTC 完美地同步了,那么其中一个时钟的相对 UTC 时间可以直接与另
外一个相比较。但是当然,完全的同步是不可能的。两个时钟都有各自独立的可变性因
素,导致它们与 UTC 之间以及它们互相之间产生误差。

 

用计算机测量时间

要想在计算机上制作一个时钟需要一个周期性的振动源——最好有很好的精确性和正确
性——以及一种让软件获取振动源的时标的方法。要想专门为了计时而制造一台计算机是
很容易的。不过,多数现在流行的计算机体系结构在设计时都没有考虑过要提供很好的时
钟。我将会结合 PC 体系结构和微软的 Windows 操作系统讲解问题所在。Linux 和嵌入式
平台上也存在类似的问题。
PC 时钟电路核心部分的晶体振荡器的基本精度是 100PPM,即 0.01%,或者每天约 8 秒的
误差。虽然这个精度只比数字腕表的精度高一点点,但对性能测量来说已经足够了,因为
对于极其非正式的测量结果,精确到几个百分点就可以了。廉价的嵌入式处理器的时钟电
路的精确度较低,但是最大的问题并非周期性振动的振动源,更困难的是如何让程序得到
可靠的时标计数值。

1. 硬件时标计数器的发展
起初的 IBM PC 是不包含任何硬件时标计数器的。它确实有一个记录一天之中的时间的
时钟,软件也可以读取这个时间。最早的微软的 C 运行时库复制了 ANSI C 库,提供了
time_t time(time_t*) 函数。该函数会返回一个距离 UTC 时间 1970 1 1 0:00 的秒
数。旧版本的 time() 函数返回的是一个 32 位有符号整数,但是在经历了 Y2K3 之后,它被
修改成了一个 64 位的有符号整数。
起初的 IBM PC 会使用来自交流电源的周期性的中断来唤醒内核去进行任务切换或是进行
其他内核操作。在北美,这个周期是 16.67 毫秒,因为交流电源是 60Hz 的。如果交流电
源是 50Hz 的话,这个周期就是 20 毫秒。

Windows 98(可能更早)以来,微软的 C 运行时提供了 ANSI C 函数 clock_t clock()
该函数会返回一个有符号形式的时标计数器。常量 CLOCKS_PER_SEC 指定了每秒钟的时标的
次数。返回值为 -1 表示 clock() 不可用。clock() 会基于交流电源的周期性中断记录时标。
clock() Windows 上的实现方式与 ANSI 所规定的不同,在 Windows 上它所测量的是经
过时间而非 CPU 时间 4。最近,clock() 被根据 GetSystemTimeAsfileTime() 重新实现了。在
2015 年时它的时标是 1 毫秒,分辨率也是 1 毫秒。这使得它成了 Windows 上一个优秀的
毫秒级别的时钟.

Windows 2000 开始,可以通过调用 DWORD GetTickCount() 来实现基于 A/C 电源中断的
软件时标计数器。GetTickCount() 的时标计数值取决于 PC 的硬件,可能会远比 1 毫秒长。
GetTickCount() 会进行一次将时标转换为毫秒的计算来消除部分不确定性。这个方法的一
个升级版是 ULONGLONG GetTickCount64(),它会以 64 位无符号整数的形式返回相同的时标

计数值,这样可以测量更长的处理时间。虽然没有办法知道当前的中断周期,但下面这对
函数可以缩短和然后恢复周期:
MMRESULT timeBeginPeriod(UINT)
MMRESULT timeEndPeriod(UINT)
这两个函数作用于全局变量上,会影响所有的进程和其他函数,如取决于交流电源的中断
周期的 Sleep()。另外一个函数 DWORD timeGetTime() 可以通过另一种方法获取相同的时标
计数值。

 

奔腾体系结构后,英特尔提供了一个叫作时间戳计数器Time Stamp CounterTSC)的
硬件寄存器。TSC 是一个从处理器时钟中计算时标数的 64 位寄存器。RDTSC 指令可以非常
快地访问该寄存器。
Windows 2000 问 世 后, 可 以 通 过 调 用 函 数 BOOL Query PerformanceCounter(LARGE_
INTEGER*) 来读取 TSC,这将会产生一次特殊的不带分辨率的时标计数。可以通过调用
BOOL QueryPerformanceFrequency(LARGE_INTEGER*) 来获得分辨率,它会返回每秒钟时标的
频率。LARGE_INTEGER 是一个带有有符号格式的 64 位整数的结构体,因为在当时引入了以
上这些函数的 Visual Studio 中还没有原生的 64 位有符号整数类型。
初始版本的 QueryPerformanceCounter() 的一个问题是,它的时标速率取决于处理器的时
钟。不同处理器和主板的处理器时钟不同。在当时,老式的 PC,特别是那些使用超微半
导 体 公 司(Advanced Micro DevicesAMD) 处 理 器 的 PC 是 没 有 TSC 的。 在 当 时 没 有
TSC 可用的情况下,QueryPerformanceCounter() 会返回 GetTickCount() 返回的低分辨率的
时标计数值。


Windows 2000 中还新增加了一个 void GetSystemTimeAsfileTime(fiLETIME*) 函数,它
会返回一个自 1601 1 1 00:00 UTC 开始计算的以 100 纳秒为时标的计数值。其中,
fiLETIME 也是一个带有 64 位整数的结构体,不过这次是无符号的形式。尽管该时标计数
器显示出来的分辨率看起来非常高,有些实现却使用了与 GetTickCount() 所使用的低分辨
率计数器相同的计数器。
很快,QueryPerformanceCounter() 的更多问题暴露出来了。有些处理器实现了可变时钟
频率来管理功耗。这会导致时标周期发生了变化。在拥有多个独立处理器的多处理器系统
中,QueryPerformanceCounter() 返回的值取决于线程运行于哪个处理器之上。处理器开
始实现指令重排序之后,导致 RDTSC 指令可能会发生延迟,降低使用了 TSC 的软件的
准确性。
为了解决这些问题,Windows Vista QueryPerformanceCounter() 使用了一种不同的计
数器,称为 Advanced Configuration and Power InterfaceACPI)电源管理计时器。使用
这个计数器虽然能够解决多处理器的同步问题,但是却显著地增加了延迟。与此同时,
英特尔重新指定了 TSC 为最大且不变的时钟频率。此外,英特尔还增加了不可重排序的
RDTSCP 指令。
Windows 8 开始,Windows 提供了一种基于 TSC 的、可靠的、高分辨率的硬件时标计
数。只要该系统运行于 Windows 8 或者之后的版本上,void GetSystemTimePreciseAsfileT
ime(fiLETIME*) 就可以生成一个固定频率和亚微秒准确度的高分辨率时标。
 一句话总结本堂历史课的内容就是,PC 从来都不是设计作为时钟的,因此它们提供的时
标计数器是不可靠的。如果以过去 35 年的历史为鉴,未来的处理器和操作系统可能依然
无法提供稳定的、高分辨率的时标计数值。
历 代 PC 都 提 供 的 唯 一 可 靠 的 时 标 计 数 器 就 是 GetTickCount() 返 回 的 时 标 计 数 器
了, 尽 管 它 也 有 缺 点。clock() 返 回 的 毫 秒 级 的 时 标 更 好, 而 且 近 10 年 生 产 的 PC
应 该 都 是 支 持 该 函 数 的。 如 果 只 考 虑 Windows 8 及 之 后 的 版 本 和 新 的 处 理 器 的 话,
GetSystemTimePreciseAsfileTime() 返回的 100 纳秒级别的时标计数器是非常精确的。不
过,就我个人的经验来看,对时间测量来说毫秒级别的准确性已经足够了。
 
2. 返转
返转wraparound)是指当时钟的时标计数器值到达最大值后,如果再增加就变为 0
过程。12 小时制的模拟时钟在每天的正午和午夜各会进行一次返转。Windows 98 在连续
运行 49 天后会因 32 位毫秒时标计数器的返转而挂起(请参见 Q216641。当两位数的年
份返转时会发生 Y2K 问题。玛雅日历在 2012 年返转,因为玛雅人认为那就是世界末日。
UNIX 时间戳(自 UTC1970 1 1 00:00 起的带符号的 32 位秒数)会在 2038 1
发生返转,这可能会称为某些“历史悠久”的嵌入式系统的“世界末日”。返转的问题出
在缺少额外的位去记录数据,导致下次时间增加后的数值比上次时间的数值小。会返转的
时钟仅适用于测量持续时间小于返转间隔的时间。
例如,在 Windows 上,GetTickCount() 函数会返回一个分辨率为 1 毫秒的 32 位无符号的
整数值作为时标计数值。那么,GetTickCount() 的返回值会每 49 天返转一次。也就是说,
GetTickCount() 适用于测量那些所需时间小于 49 天的操作。如果一个程序在某个操作开始
时和结束时分别调用了 GetTickCount(),两个返回值之间的差值就是两次调用之间经过的
毫秒数。例如:
DWORD start = GetTickCount();
DoBigTask();
DWORD end = GetTickCount();
cout << "Startup took " << end-start << " ms" << endl;
C++ 实现无符号算术的方式去确保了即使在发生返转时也可以得到正确的结果。
GetTickCount() 对于记住自程序启动后所经过的时间是比较低效的。许多“历史悠久”的
服务器可以持续运行数个月甚至数年。返转的问题在于,由于缺少位数去记录返转的次
数,end-start 的结果中可能体现不出发生了返转,或是体现出一个或者多个返转。
Windows Vista 开始,微软加入了 GetTickCount64() 函数,它会返回一个 64 位无符号且
显示分辨率为 1 毫秒的时标计数值。GetTickCount64() 的结果只有在数百万年后才会发生
返转。这就意味着,几乎不会有人能够见证返转发生了。
3. 分辨率不是准确性
Windows 上,GetTickCount() 会返回一个无符号的 32 位整数值。如果一个程序在某个
操作开始和结束时分别调用了 GetTickCount(),两个返回值之间的差值就是两次调用之间
经过的毫秒单位的执行时间。因此,GetTickCount() 的分辨率是 1 毫秒。
例如,下面这段代码通过在循环中反复调用 Foo(),在 Windows 上测量了名为 Foo() 的函
 数的相对性能。通过在代码块开始和结束时得到的时标计数值,我们可以计算出循环处理
所花费的时间:
DWORD start = GetTickCount();
for (unsigned i = 0; i < 1000; ++i) {
Foo();
}
DWORD end = GetTickCount();
cout << "1000 calls to Foo() took " << end-start << "ms" << endl;
如果 Foo() 中包含了大量的计算,那么这段代码的输出结果可能如下:
1000 calls to Foo() took 16ms
不幸的是,从微软网站中关于 GetTickCount() 的文档(https://msdn.microsoft.com/en-us/
library/windows/desktop/ms724408(v=vs.85).aspx)来看,调用 GetTickCount() 的准确性
可能是 10 毫秒或 15.67 毫秒。也就是说,如果连续调用两次 GetTickCount(),那么结果
之间的差值可能是 0 或者 1 毫秒,也可能是 1015 16 毫秒。因此,测量的基础精度
15 毫秒,额外的分辨率毫无价值。之前代码块的输出结果可能会是 10ms20ms 或精
确的 16ms
GetTickCount() 特 别 让 人 沮 丧 的 一 点 是, 除 了 分 辨 率 是 1 毫 秒 外, 无 法 确 保 在 两 台
Windows 计算机中该函数是以某种方式或是相同方式实现的。
我在 Windows 上测试了许多计时函数,试图找出它们在某一台计算机(基于 i7 处理器的
Surface 3 平板电脑)的某个操作系统(Windows 8.1)上的可用分辨率。示例代码 3-1 中的
测试循环地调用了测量时间的函数,并检查这些连续的函数调用的返回值之间的差值。如
果时标的可用分辨率大于函数调用的延迟,那么这些连续的函数调用的返回值将要么相
同,要么它们之间的差值是若干个基础时标,单位是函数的分辨率。我计算了非零差值的
平均值,以排除操作系统偷用时间片段去执行其他任务的误差。

3-1 测量 GetTickCount() 的时标
unsigned nz_count = 0, nz_sum = 0;
ULONG last, next;
for (last = GetTickCount(); nz_count < 100; last = next) {
next = GetTickCount();
if (next != last) {
nz_count += 1;
nz_sum += (next - last);
}
}
std::cout << "GetTickCount() mean resolution "
<< (double)nz_sum / nz_count
<< " ticks" << std::endl;
我将测量结果总结在了表 3-1 中。
)上测量的时标结果
函数 时标
time() 1
GetTickCount() 15.6 毫秒
GetTickCount64() 15.6 毫秒
timeGetTime() 15.6 毫秒
clock() 1.0 毫秒
GetSystemTimeAsFileTime() 0.9 毫秒
GetSystemTimePreciseAsFileTime() 450 纳秒
QueryPerformanceCounter() 450 纳秒
需 要 特 别 注 意 的 是 GetSystemTimeAsfileTime() 函 数。 它 的 显 示 分 辨 率 是 100 纳 秒, 但
是看起来却似乎是基于同样低分辨率的 1 毫秒时标的 clock() 实现的,而 GetSystemTime
PreciseAsfileTime() 看起来则是用 QueryPerformanceCounter() 实现的。
现代计算机的基础时钟周期已经短至了数百皮秒(100 皮秒是 10-10 秒)。它们可以以几纳
秒的速度执行指令。但是在这些 PC 上却没有提供可访问的皮秒级或是纳秒级的时标计数
器。在 PC 上,可使用的最快的时标计数器的分辨率是 100 纳秒级的,而且它们的基础准
确性可能远比它们的分辨率更低。这就导致不太可能测量函数的一次调用的持续时间。读
者可以参见 3.4.3 节看看如何应对这个问题。
 
延迟
延迟是指从发出命令让活动开始到它真正开始之间的时间。延迟是从丢下一枚硬币到井水
中到听见井水溅落之间的时间。它也是发令员鸣枪至选手出发之间的时间。
就计算机上的时间测量而言,之所以会有延迟是因为启动时钟、运行实验和停止时钟是一
系列的操作。整个测量过程可以分解为以下五个阶段。
(1)“启动时钟”涉及调用函数从操作系统中获取一个时标计数。这个调用的时间大于 0
在函数调用过程中,会实际地从处理器寄存器中获取时标计数器的值。这个值就是开始
时间。我们称其为间隔 t1
(2) 在读取时标计数器的值后,它仍然必须被返回和赋值给一个变量。这些动作也需要花费
时间。实际的时钟已经在计时的过程中了,但是时标计数值还没有增加。我们称其为间
t2
(3) 测量实验开始,然后结束。我们称其为间隔 t3
(4)“停止时钟”涉及另外一个函数调用去获取一个时标计数值。虽然实验已经结束了,但
是在函数运行至读取时标计数值的过程中,计时器仍然在计时。我们称其为间隔 t4
(5) 读取时标计数器的值后,它仍然必须被返回和赋值给一个变量。这时,虽然时钟仍然在
计时,但是由于已经读取了时标计数器的值,因此测量结果并不会继续错误地累加。我
们称其为间隔 t5
因此,虽然实际上测量时间应当是 t3,但测量到的值却更长一些,是 t2+t3+t4。因此,延迟就
t2+t4。如果延迟对相对实验运行时间的比例很大,实验员必须从实验结果中减去延迟。
假设获取一次时标计数值的时间是 1 微秒(μs,而且时标计数值是由当时执行的最后一条
指令获得的。在以下这段伪代码中,直到第一次函数调用的最后一条指令调用 get_tick()
才开始测量时间,因此在测量活动之前是没有延迟的。在测试的最后调用 get_tick() 的延
迟则被计算到了测量结果中:
start = get_tick() // 测量开始之前的1μs延迟没有影响
do_activity()
stop = get_tick() // 测量开始之后的1μs延迟被计算到测量结果中
duration = stop-start
如 果 被 测 量 的 活 动 的 执 行 时 间 是 1 微 秒, 那 么 测 量 结 果 就 是 2 微 秒, 误 差 达 将 会 到
100%;而如果被测量的活动的执行时间是 1 毫秒,那么测量结果就是 1.001 微秒,误差
只有 0.1%
如果同一个函数既在实验前被调用了,也在实验后被调用了,那么有 t1=t4 t2=t5。也就是
说,延迟就是计时函数的执行时间。
我在 Windows 上测试了计时函数的调用延迟,也就是它们的执行时间。代码清单 3-2 展示
了一个典型的用于计算 GetSystemTimeAsfile() 函数的时间的测试套件。
代码清单 3-2 Windows 计时函数的延迟
ULONG start = GetTickCount();
LARGE_INTEGER count;
for (counter_t i = 0; i < nCalls; ++i)
QueryPerformanceCounter(&count);
ULONG stop = GetTickCount();
std::cout << stop - start
<< "ms for 100m QueryPerformanceCounter() calls"
<< std::endl;
 
Windows计时函数的延迟(2013i7Win 8.1
函数 执行时间
GetSystemTimeAsFileTime() 2.8 纳秒
GetTickCount() 3.8 纳秒
GetTickCount64() 6.7 纳秒
QueryPerformanceCounter() 8.0 纳秒
clock() 13 纳秒
time() 15 纳秒
TimeGetTime() 17 纳秒
GetSystemTimePreciseAsFileTime() 22 纳秒
 

测试结果中非常有趣的地方是,在我的 i7 平板电脑上,所有的延迟都在若干纳秒的范围
内。所以,这些函数调用是相当高效的。这就意味着延迟不会对在循环中连续调用函数约
1 秒的测量结果的准确性造成影响。不过,对于那些读取相同的低分辨率时标的函数,这
些时间开销的差距仍然在 10 倍左右。GetSystemTimePreciseAsfileTime() 的延迟最高,而
这个最高的延迟相对于它的时标占到了大约 5%。延迟问题在慢速处理器上更严重。


5. 非确定性行为
计算机是带有大量内部状态的异常复杂的装置,其中绝大多数状态对开发人员是不可见的。
执行函数会改变计算机的状态(例如高速缓存中的内容),这样每次重复执行指令时,情况
都会与前一条指令不同。因此,内部状态的不可控的变化是测量中的一个随机变化源。
而且,操作系统对任务的安排也是不可预测的,这样在测量过程中,在处理器和内存总线
上运行的其他活动会发生变化。这会降低测量的准确性。
操作系统甚至可能会暂停执行正在被测量的代码,将 CPU 时间分配给其他程序。但是在
暂停过程中,时标计数器仍然在计时。这会导致与操作系统没有将 CPU 时间分配给其他
程序相比,测量出的执行时间变大了。这是一种会对测量造成更大影响的随机变化源。