我听说过这个词,但我不完全确定它是什么意思,所以:
- 它是什么意思,什么不是它的意思?
- 什么是什么是和什么不是微基准测试有哪些例子?
- 微基准测试有哪些危险以及如何避免?
- (或者这是一件好事?)
我听说过这个词,但我不完全确定它是什么意思,所以:
它的意思正是它在锡罐上所说的 - 它正在测量“小”事物的性能,例如对操作系统内核的系统调用。
危险在于人们可能会使用从微基准测试中获得的任何结果来指示优化。众所周知:
我们应该忘记小的效率,比如大约 97% 的时间:过早的优化是万恶之源”——Donald Knuth
可能有许多因素会影响微基准测试的结果。编译器优化就是其中之一。如果被测量的操作花费的时间太短以至于你用来测量它的时间比实际操作本身要长,那么你的微基准也会出现偏差。
例如,有人可能会对for
循环的开销进行微基准测试:
void TestForLoop()
{
time start = GetTime();
for(int i = 0; i < 1000000000; ++i)
{
}
time elapsed = GetTime() - start;
time elapsedPerIteration = elapsed / 1000000000;
printf("Time elapsed for each iteration: %d\n", elapsedPerIteration);
}
显然,编译器可以看到循环完全没有做任何事情,并且根本不会为循环生成任何代码。所以elapsed
and的值elapsedPerIteration
几乎没有用。
即使循环做了一些事情:
void TestForLoop()
{
int sum = 0;
time start = GetTime();
for(int i = 0; i < 1000000000; ++i)
{
++sum;
}
time elapsed = GetTime() - start;
time elapsedPerIteration = elapsed / 1000000000;
printf("Time elapsed for each iteration: %d\n", elapsedPerIteration);
}
编译器可能会看到该变量sum
不会用于任何事情并将其优化掉,并优化掉 for 循环。可是等等!如果我们这样做会怎样:
void TestForLoop()
{
int sum = 0;
time start = GetTime();
for(int i = 0; i < 1000000000; ++i)
{
++sum;
}
time elapsed = GetTime() - start;
time elapsedPerIteration = elapsed / 1000000000;
printf("Time elapsed for each iteration: %d\n", elapsedPerIteration);
printf("Sum: %d\n", sum); // Added
}
编译器可能足够聪明,可以意识到它sum
始终是一个常量值,并优化所有这些。如今,许多人会对编译器的优化能力感到惊讶。
但是编译器无法优化的东西呢?
void TestFileOpenPerformance()
{
FILE* file = NULL;
time start = GetTime();
for(int i = 0; i < 1000000000; ++i)
{
file = fopen("testfile.dat");
fclose(file);
}
time elapsed = GetTime() - start;
time elapsedPerIteration = elapsed / 1000000000;
printf("Time elapsed for each file open: %d\n", elapsedPerIteration);
}
即使这不是一个有用的测试!操作系统可能会看到文件被非常频繁地打开,因此它可能会将其预加载到内存中以提高性能。几乎所有操作系统都这样做。当您打开应用程序时也会发生同样的事情 - 操作系统可能会找出您打开最多的前 5 个应用程序,并在您启动计算机时将应用程序代码预加载到内存中!
事实上,有无数变量在起作用:引用的局部性(例如数组与链表)、缓存和内存带宽的影响、编译器内联、编译器实现、编译器切换、处理器内核的数量、处理器级别的优化、操作系统调度程序、操作系统后台进程等。
所以在很多情况下,微基准测试并不是一个有用的指标。它绝对不会用定义明确的测试用例(分析)代替整个程序的基准测试。首先编写可读代码,然后配置文件以查看需要做什么(如果有)。
我想强调一下,微基准本身并不是邪恶的,但必须小心使用它们(对于与计算机相关的许多其他事情都是如此)
微基准测试没有定义,但当我使用它时,我指的是一个小型人工基准测试,旨在测试某些特定硬件1或语言功能的性能。相比之下,更好的基准测试是为执行实际任务而设计的真实程序。(在这两种情况之间划清界限是没有意义的,IMO,我不会尝试。)
微基准测试的危险在于,很容易编写一个给出完全误导的结果的基准。Java 微基准测试中的一些常见陷阱是:
但是,即使您已经解决了上述问题,基准测试仍然存在无法解决的系统性问题。基准测试的代码和行为通常与您真正关心的内容无关;即您的应用程序将如何执行。有太多的“隐藏变量”让您无法从基准测试推广到典型程序,更不用说推广到您的程序了。
由于这些原因,我们经常建议人们不要在微基准上浪费时间。相反,最好编写简单自然的代码,并使用分析器来识别需要手动优化的区域。有趣的是,实际应用中最严重的性能问题通常是由于数据结构和算法设计不当(包括网络、数据库和线程相关的瓶颈)造成的,而不是典型的微基准测试试图解决的问题。测试。
@BalusC 在Hotspot 常见问题页面中提供了指向该主题的材料的极好链接。这里是Brian Goetz的 IBM 白皮书的链接。
1 - 专家甚至不会尝试在 Java 中进行硬件基准测试。字节码和硬件之间发生了太多“复杂的事情”,无法从原始结果中得出关于硬件的有效/有用的结论。您最好使用更接近硬件的语言;例如 C 甚至汇编代码。
- 它是什么意思,什么不是它的意思?
我想说微基准测试只是意味着测量微小的东西。Tiny 可能与上下文相关,但通常在单个系统调用或类似的级别上。基准测试是指上述所有内容。
- 什么是什么是和什么不是微基准测试有哪些例子?
这篇(已归档)文章列出了测量 getpid() 系统调用的时间和测量使用 memcpy() 复制内存的时间作为微基准测试的示例。
算法实现等的任何测量都不算作微基准测试。特别是列出执行时间减少的任务的结果报告可能很少被视为微基准测试。
- 微基准测试有哪些危险以及如何避免?
明显的危险是它诱使开发人员优化程序的错误部分。另一个危险是众所周知,准确地测量小东西是非常困难的。避免它的最简单方法可能只是了解程序中花费最多时间的地方。
人们通常会说“不要进行微基准测试”,但他们的意思可能是“不要根据微基准做出优化决策”。
- (或者这是一件好事?)
与这里的其他人一样,这本身并不是一件坏事,而且许多网页似乎都在暗示。它有它的地方。我从事程序重写和运行时方面的编织等工作。我们通常发布我们添加的指令的微基准,不是为了指导任何优化,而是确保我们的额外代码对重写程序的执行几乎没有影响。
然而,这是一门艺术,尤其是在具有 JIT、预热时间等的 VM 的上下文中。这里描述了一种很好的 Java 方法(存档)。
《Java 性能:权威指南》一书有关于微基准的定义和示例:
微基准
微基准测试是一种旨在测量非常小的单元性能的测试:调用同步方法与非同步方法的时间;创建线程与使用线程池的开销;执行一种算术算法与另一种实现的时间;等等。
微基准测试似乎是个好主意,但它们很难正确编写。考虑以下代码,它试图编写一个微基准测试来测试计算第 50 个斐波那契数的方法的不同实现的性能:
public void doTest(){ double l; long then = System.currentTimeMillis(); for(int i = 0; i < nLoops; i++){ l = fibImpl1(50); } long now = system.currentTimeMillis(); System.out.println("Elapsed time: " + (now - then)) } ... private double fibImpl1(int n){ if(n < 0) throw new IllegalArgumentException("Must be > 0"); if(n == 0) return 0d; if(n == 1) return 1d; double d = fibImpl1(n - 2) + fibImpl(n - 1); if(Double.isInfinited(d)) throw new ArithmeticException("Overflow"); return d; }
微基准测试必须使用他们的结果。
这段代码最大的问题是它实际上从未改变任何程序状态。因为从未使用过斐波那契计算的结果,编译器可以随意丢弃该计算,智能编译器(包括当前的 Java 7 和 8 编译器)最终将执行以下代码:
long then = System.currentTimeMillis(); long now = System.currentTimeMillis(); System.out.println("Elapsed time: " + (now - then));
因此,无论斐波那契方法的实现如何,或者循环应该执行的次数如何,经过的时间都将只有几毫秒。
有一种解决该特定问题的方法:确保读取每个结果,而不是简单地写入。在实践中,将 l 的定义从局部变量更改为实例变量(使用 volatile 关键字声明)将允许测量方法的性能。
以下是 Brian Goetz 的一些好文章,解释了为什么(微)基准测试在 Java 中特别难:
微基准测试是我认为不值得的基准测试。有效的基准测试是我认为值得花时间的基准测试。
一般来说,微基准测试(如 silico 所说)试图测量一些非常精细的任务的性能,这既很难做好,而且在实际性能问题的情况下通常毫无意义。