18. 测量性能
18.1. 测试速度
许多编译器有分析器,使测量一个程序中每个函数被调用多少次以及花了多长时间成为可能。找出程序里的热点十分有用。如果一个特定的热点占用了总执行时间的很大部分,那么这个热点应该是优化工作的对象。
许多分析器不是非常准确,当然对代码一小部分的精细调整不够准确。测试一段代码速度最准确的方法是使用所谓的时间戳计数器。这是一个内部的64位时钟计数器,使用指令RDTSC(read time stamp counter)可以把它读入EDX:EAX。时间戳计数器以CPU时钟频率计数,因此每个计数等于一个时钟周期,这是最小的相关时间单元。一些超频处理器以稍有不同的频率计数。
时间戳计数器非常有用,因为它可以测量一段代码花了多少时钟周期。某些处理器能根据工作负荷的改变,调整时钟频率。不过,在查找一小段代码的瓶颈时,测量时钟周期比测量毫秒信息量更大。
时间测量精度等于,在具有SpeedStep技术的Intel处理器上,时钟倍频器的值(即11)。在其他处理器上,这个精度是1时钟周期。具有SpeedStep技术的处理器,有一个性能计数器用于非停止核周期,它给出比时间戳计数器更准确的测量。启用这个计数器需要特权访问。
在所有具有乱序执行的处理器上,在每次读该计数器前后,你必须插入XOR EAX, EAX / CUPID,以防止它与其他指令并行执行。CPUID是一条串行化指令,这意味着在处理前,它冲洗流水线并等待所有挂起的操作完成。对测试目的,这非常有用。
在时钟滴答计数时,最大的问题是避免中断。受保护的操作系统不允许清除中断标记,因此在测试过程中,你不能避免中断与任务切换。这使得测试结果不准确且不可重复。有几个克服这个问题的替代方法:
- 以高优先级运行测试代码,尽量减少中断与任务切换的风险。
- 如果测试代码片段不是太长,你可以重复测试几次,并假定测得的最低时钟计数代表没有发生中断的情形。
- 如果测试代码片段很长,中断无法避免,你可以重复测试许多次,取时钟计数的平均值。
- 制作一个虚拟设备驱动来清除中断标记。
- 使用一个允许清除中断标记的操作系统(比如没有网络的Windows 98,控制台模式)。
- 使用旧式DOS操作系统,在实模式中启动测试程序。
我做了一系列使用方法1,2及可能6的测试程序。可以在www.agner.org/optimize/testp.zip得到这些程序。
在具有多个核的处理器上,如果线程可以从一个核跳到另一个,出现进一步的复杂性。不同核上的时间戳计数器不一定同步。如果采取了上述最小化中断的预防措施,在测试小片段代码时,这不是问题。但在测量更长时间间隔时,这会成为一个问题。你可能需要将进程锁定到一个CPU核,例如,使用Windows中函数SetProcessAffinityMask。这在文档《Game Timing and Multicore Processors》,Microsoft 2005,http://msdn.microsoft.com/en-us/library/ee417693.aspx中讨论。
你将很快看到,在测量时钟周期时,一段代码第一次执行时总是花更长时间,彼时它不在缓存中。另外,在分支预测器适用代码前,可能需要2或3次迭代。在代码与数据不在缓存时,第一次测量给出执行时间。后续测量给出可能具有最好缓存的执行时间。
PPro、P2与P3处理器上的对齐效应使这些处理器上的测量非常困难。假设你有一段代码,你希望做一个预期使代码快几个时钟周期的修改。修改后的代码与原来代码不一样大。这意味着修改后代码将有不同的对齐,指令获取块将不同。如果指令获取与就解码是瓶颈——在这些处理器上通常是,那么对齐的改变可能使代码快或慢上几个时钟周期。对齐的改变对时钟计数的影响,可能实际上比你的修改更大。因此,你可能不能验证修改本身使代码变快还是变慢。预测每个指令获取块在哪里开始,相当困难,如手册3《Intel,AMD与VIA CPU的微架构》所述。
其他处理器没有严重的对齐问题。不过,P4确实有类似,但不那么严重的影响。这个影响由追踪缓存中μop对齐改变导致。如果追踪缓存是瓶颈,在一个条件跳转指令后,跳到最不常用(但预测成功)分支所需时间,对不同的对齐,差异最多可到2时钟周期。追踪缓存中μop的对齐很难预测。
大多数x86处理器还要一组所谓的性能监控计数器,可以统计比如缓存不命中、对齐错误、分支误预测等。这些对分析性能问题十分有用。性能监控计数器是特定于处理器的。对每种CPU,你需要不同的测试设置。
性能监控计数器的细节可以在Intel的《IA-32 Intel Architecture Software Developer’s Manual》卷3以及AMD的《BIOS and Kernel Developer's Guide》中找到,、
你需要特权访问来设置性能监控计数器。使用设备驱动是最方便的。在www.agner.org/optimize/testp.zip的测试程序,给出了在32位与64位Windows以及16位实模式DOS下,对性能监控计数器的访问。这些测试程序支持在大多数Intel,AMD与VIA处理器中不同类型的性能监控计数器。
Intel与AMD提供了使用它们各自处理器性能监控计数器的分析器。Intel的分析器称为VTune,AMD的分析器叫CodeAnalyst。
18.2. 单元测试的陷阱
如果你希望找出一个函数的哪个版本性能最好,在一个调用这个函数许多次的小测试程序中测量时钟周期是不够的。这样一个测试不太可能给出缓存不命中的真实测量,因为测试程序可能使用不超过缓存大小的内存。例如,一个孤立的测试可能显示展开一个循环是有利的,而一个该函数放入最终程序的测试表明,在展开循环时,有大量的缓存不命中。
因此,在评估一个函数的性能时,不仅统计时钟周期,而且考虑它在代码缓存、数据缓存及分支历史缓冲里,使用了多少空间,是重要的。
这个问题的进一步讨论,参考手册1《优化C++软件》中“单元测试陷阱”一节。
19. 参考文献
本手册是第1页中介绍提到的在www.agner.org/optimize可获取的系列的部分。特定处理器的相关文献列表,参考手册3《Intel,AMD与VIA CPU的微架构》。
许多其他来源也有有用的信息。这些来源列在comp.lang.asm.x86新闻组的FAQ里,其他网络来源,访问www.agner.org/optimize里的链接。
一些有用的书籍:
R. C. Detmer: Introduction to 80x86 Assembly Language and Computer Architecture, 2'nd ed. Jones & Bartlett, 2006. Jones & Bartlett, 2006.
汇编编程好的介绍
J. L. Hennessy and D. A. Patterson: Computer Architecture: A Quantitative Approach, 3'rd ed. 2002.
计算机架构与微架构的好教材
John R. Levine: Linkers and Loaders. Morgan Kaufmann, 2000.
解释了链接器与载入器如何工作。
Henry S. Warren, Jr.: "Hacker's Delight". Addison-Wesley, 2003.
包含了许多比特操作技巧