16 Testing speed

测试程序的速度是优化工作的重要部分。您必须检查您的修改是否确实增加了速度。
有各种各样的性能分析工具可用于找出热点并测量程序的整体性能。然而,这些分析工具并不总是准确的,当程序大部分时间都在等待用户输入或读取磁盘文件时,很难精确地测量您想要的内容。请参阅第16页中关于性能分析的讨论。
当确定了一个热点之后,将热点分离并仅对代码的这一部分进行测量可能是有用的。可以通过使用所谓的时间戳计数器来完成此操作,该计数器使用CPU时钟的分辨率。时间戳计数器是一个计数器,它测量自CPU启动以来的时钟脉冲数量。一个时钟周期的长度是时钟频率的倒数,如第15页所解释的那样。如果在执行关键代码片段之前和之后读取时间戳计数器的值,那么您可以得到准确的时间消耗,即两个时钟计数之间的差异。
可以使用下面示例16.1中列出的ReadTSC函数获取时间戳计数器的值。这段代码只适用于支持内嵌函数的编译器。或者,您可以从www.agner.org/optimize/testp.zip下载头文件timingtest.h,或者从www.agner.org/optimize/asmlib.zip获取ReadTSC作为库函数。

// Example 16.1
#include <intrin.h> // Or #include <ia32intrin.h> etc.
long long ReadTSC() { // Returns time stamp counter
int dummy[4]; // For unused returns
volatile int DontSkip; // Volatile to prevent optimizing
long long clock; // Time
__cpuid(dummy, 0); // Serialize
DontSkip = dummy[0]; // Prevent optimizing away cpuid
clock = __rdtsc(); // Read time
return clock;
}

您可以使用此函数在执行关键代码前后测量时钟计数。测试设置可能如下所示:

// Example 16.2
#include <stdio.h>
#include <asmlib.h> // Use ReadTSC() from library asmlib..
// or from example 16.1
void CriticalFunction(); // This is the function we want to measure
...
const int NumberOfTests = 10; // Number of times to test
int i; long long time1;
long long timediff[NumberOfTests]; // Time difference for each test
for (i = 0; i < NumberOfTests; i++) { // Repeat NumberOfTests times
time1 = ReadTSC(); // Time before test
CriticalFunction(); // Critical function to test
timediff[i] = ReadTSC() - time1; // (time after) - (time before)
}
printf("\nResults:"); // Print heading
for (i = 0; i < NumberOfTests; i++) { // Loop to print out results
printf("\n%2i %10I64i", i, timediff[i]);
}

示例16.2中的代码调用了关键函数10次,并将每次运行的时间消耗存储在数组中。然后,在测试循环之后输出值。通过这种方式测量的时间包括调用ReadTSC函数所需的时间。您可以从计数中减去此值。这可以通过删除Example 16.2中CriticalFunction的调用来简单地测量。
以下是对测量时间的解释。通常情况下,第一个计数比随后的计数高。这是在代码和数据未缓存时执行CriticalFunction所需的时间。后续计数给出了尽可能缓存代码和数据的情况下的执行时间。第一个计数和随后的计数表示“最坏情况”和“最佳情况”值。哪一个值更接近真相取决于CriticalFunction在最终程序中被调用一次还是多次以及是否有其他代码在调用CriticalFunction之间使用缓存。如果您的优化工作集中在CPU效率上,则应查看“最佳情况”计数,以查看某个修改是否有利可图。另一方面,如果您的优化工作集中在排列数据以提高缓存效率方面,那么您还可以查看“最坏情况”计数。无论如何,时钟计数都应乘以时钟周期和CriticalFunction在典型应用程序中被调用的次数,以计算最终用户可能经历的时间延迟。
偶尔,测量到的时钟计数远高于正常值。这种情况发生在执行CriticalFunction时发生任务切换时。您无法在受保护的操作系统中避免这种情况,但可以通过在测试前增加线程优先级并在测试后将优先级设置回正常来减少问题。
时钟计数常常波动,并且可能很难获得可重现的结果。这是因为现代CPU可以根据工作负载动态改变其时钟频率。当工作负载高时,时钟频率会增加,当工作负载低时,时钟频率会降低以节省功率。有各种方法可获得更可重复的时间测量:
•通过在要测试的代码之前立即给CPU一些重负载的工作来预热CPU。
•在BIOS设置中禁用节能选项。
•对于英特尔CPU:使用内核时钟周期计数器(见下文)
16.1 Using performance monitor counters
许多CPU都具有称为性能监视器计数器的内置测试功能。性能监视器计数器是CPU内部的计数器,可以设置为计数特定事件,例如执行的机器指令数量、缓存未命中、分支错误预测等。这些计数器对于调查性能问题非常有用。性能监视器计数器是特定于CPU的,每个CPU型号都有自己的性能监视选项。
CPU供应商提供适合其CPU的分析工具。英特尔的分析工具称为VTune;AMD的分析工具称为CodeAnalyst。这些分析工具用于识别代码中的热点。
为了自己的研究,我开发了一个使用性能监视器计数器的测试工具。我的测试工具支持英特尔、AMD和VIA处理器,并可从www.agner.org/optimize/testp.zip获得。此工具不是分析工具。它不是用于查找热点的,而是用于在确定热点后研究一段代码。
我的测试工具可以以两种方式使用。第一种方法是将要测试的代码段插入到测试程序本身中并重新编译它。我正在使用此方法来测试单个汇编指令或小的代码序列。第二种方法是在运行要优化的程序之前设置性能监视器计数器,并在要测试的代码段之前和之后读取性能计数器。您可以使用与示例16.2类似的原理,但是读取一个或多个性能监视器计数器而不是(或除了)时间戳计数器。测试工具可以在所有CPU核心中设置并启用一个或多个性能监视器计数器,并将其保留启用状态(每个CPU核心都有一组计数器)。计数器将保持开启,直到您将其关闭或计算机重置或进入睡眠模式。详细信息请参阅我的测试工具手册(www.agner.org/optimize/testp.zip)。
英特尔处理器中特别有用的一个性能监视器计数器称为核心时钟周期。核心时钟周期计数器计数CPU核心运行的实际时钟频率下的时钟周期,而不是外部时钟。这提供了几乎独立于时钟频率变化的测量结果。核心时钟周期计数器在测试哪个版本的代码最快时非常有用,因为您可以避免时钟频率上升和下降的问题。
请记住,在不进行测试时插入一个开关来关闭计数器的读取。尝试在计数器被禁用时读取性能监视器计数器将导致程序崩溃。
16.2 The pitfalls of unit-testing
在软件开发中,对每个函数或类进行单独测试是常见的做法。这种单元测试对于验证优化函数的功能是必要的,但不幸的是,单元测试并不能完全提供有关函数性能(速度)的全部信息。
假设您有两个不同版本的关键函数,并且您想找出哪个版本最快。通常的测试方法是创建一个小型的测试程序,使用适当的测试数据多次调用关键函数,并测量所需的时间。在这种单元测试中,表现最佳的版本可能具有比替代版本更大的内存占用。由于代码和数据内存的总量很可能小于缓存大小,在单元测试中无法观察到缓存未命中的影响。
当关键函数被插入到最终程序中时,代码缓存、微操作缓存和数据缓存很可能成为关键资源。现代CPU速度如此之快,以至于执行指令所花费的时钟周期很少成为瓶颈,而内存访问和缓存大小更有可能成为瓶颈。如果情况属实,那么在单元测试中花费更长时间但内存占用较小的版本可能是最优的关键函数版本。
例如,如果您想确定是否有利于展开一个大循环,就不能仅依靠单元测试而不考虑缓存效应。
您可以通过查看链接映射或汇编列表计算函数使用了多少内存。使用链接器的“生成映射文件”选项。代码缓存使用和数据缓存使用都可能很关键。分支目标缓存也是一个可能很关键的缓存。因此,函数中跳转、调用和分支的次数也应该考虑在内。
一个真实的性能测试应该包括不仅仅是一个单独的函数或热点,还应该包括包含关键函数和热点的最内层循环。测试应该使用真实的数据集进行,以便获得关于分支预测错误的可靠结果。性能测量不应包括任何等待用户输入的程序部分。文件输入和输出的时间应该单独测量。
很遗憾,通过单元测试来衡量性能的误解非常普遍。甚至一些最优化的函数库也会使用过多的循环展开,导致内存占用不合理地过大。
16.3 Worst-case testing
大多数性能测试都是在最佳情况下进行的。所有干扰因素都被排除,所有资源都足够,并且缓存条件是最佳的。最佳情况测试是有用的,因为它可以提供更可靠和可重复的结果。如果您想比较同一算法的两种不同实现的性能,那么您需要消除所有干扰因素,以使测量结果尽可能准确和可重复。
然而,在某些情况下,在最坏情况下测试性能更为相关。例如,如果您想确保对用户输入的响应时间始终不超过可接受的限制,那么应该在最坏情况下测试响应时间。
为了确保产生流式音频或视频的程序始终按照预期的实时速度工作,还应该在最坏情况下进行测试。输出中的延迟或故障是不可接受的。
以下每种方法在测试最坏情况下的性能时都可能是相关的:
- 第一次激活程序的特定部分时,由于代码的延迟加载、缓存未命中和分支预测错误,它很可能比后续的运行更慢。
- 对整个软件包进行测试,包括所有运行时库和框架,而不是仅孤立测试单个函数。在不同的软件包部分之间切换,以增加程序代码的某些部分未被缓存甚至被交换到磁盘的可能性。
- 针对依赖网络资源和服务器的软件,应该在网络负载较重且服务器处于满负荷使用状态的网络上进行测试,而不是在专用测试服务器上进行测试。
- 使用大型数据文件和包含大量数据的数据库进行测试。
- 使用旧计算机,配备较慢的CPU、不足的RAM,安装了大量无关的软件,运行了大量后台进程,并且硬盘速度较慢且碎片化。
- 使用不同品牌的CPU、不同类型的显卡等进行测试。
- 使用会在访问时扫描所有文件的防病毒程序。
- 同时运行多个进程或线程。如果微处理器支持超线程,则尝试在同一处理器核心中运行两个线程。
- 尝试分配比可用内存更多的RAM,以强制将内存交换到磁盘。
- 通过使最内层循环中使用的代码大小或数据大小大于缓存大小来诱发缓存未命中。或者,您可以主动使缓存失效。操作系统可能具有此目的的函数,或者您可以使用_mm_clflush内部函数。
- 通过使数据比正常情况更随机来诱发分支预测错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值