测量长时间运行的代码
如果程序只是运行一个计算密集型的任务,那么分析器会自动地告诉我们程序中的热点在
哪里。不过如果程序要做许多不同的处理,可能在分析器看来,没有任何一个函数是热
点。程序还有可能会花费大量的时间等待 I/O 或是外部事件,这样降低了程序的性能,增
加了程序的实际运行时间。在这种情况下,我们需要测量程序中各个部分的时间,然后试
着减少其中低效部分的运行时间。
开发人员通过不断地缩小长时间运行的任务的范围直至定位其中一段代码花费了太长时
间,感觉不对劲这种方式来查找代码中的热点。在找出这些可疑代码后,开发人员会在测
试套件中对小的子系统或是独立的函数进行优化实验。
测量运行时间是一种测试关于“如何减少某个特定函数的性能开销”的假设的有效方式。
一般,我们很难意识到可以通过编程在计算机上实现秒表功能。你可以非常方便地使用手
机或是手提电脑在工作日的 6:45 叫醒你,或是在早上 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°F、310°F、315°F 等。以一度为单
位显示温度应该比恒温器的单位更合理。有效分辨率只有 5°F 只是表示测量的最低有效位
只能是 0 或者 5。
当读者知道他们身边廉价的温度计、尺子和其他测量设备的有效分辨率后可能会感到吃惊
和失望,因为这些设备的显示分辨率是 1 个单位或是 1/10 单位。
用多个时钟测量
当两个事件在同一个地点发生时,很容易通过一个时钟的时标计数来测量事件的经过时
间。但是如果这两个事件发生在相距很远的不同地点,可能就需要两个时钟来测量时间。
而两个不同时钟的时标次数无法直接比较。
人类想到了一个办法,那就是通过与国际协调时间(Coordinated Universal Time)同步。
国际协调时间与经度 0 度的天文学上的午夜同步,而经度 0 度这条线穿过了英格兰格林威
治皇家天文台中的一块漂亮的牌匾(请参见图 3-5)。这样就可以将一个以时标计数值表示
的时间转换为以时分秒表示的相对 UTC(Universal 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 上一个优秀的
毫秒级别的时钟.
自