什么是微基准?

我听说过这个术语,但我不完全确定它是什么意思,所以:

  • 这是什么意思,什么不是什么意思?
  • 什么是IS和IS不是微基准的一些例子?
  • microbenchmarking有什么危险,你如何避免它?
    • (或者是件好事?)

它的意思正是它在锡jar上所说的 – 它正在测量“小”的性能,比如系统调用操作系统的内核。

危险在于人们可以使用从微观基准标记中获得的任何结果来指示优化。 众所周知,

我们应该忘记小效率,大约97%的时间:不成熟的优化是所有邪恶的根源 – Donald Knuth

可能有很多因素会影响微基准的结果。 编译器优化就是其中之一。 如果被测量的操作花费的时间太less,无论用什么来测量它,都会比实际操作本身花费更长的时间,那么您的微基准也会被扭曲。

例如,有人可能会采取for循环开销的microbenchmark:

 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); } 

很显然,编译器可以看到循环完全没有任何问题,也没有为循环生成任何代码。 所以elapsedelapsedPerIteration的值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); } 

编译器可能会看到variablessum不会被用于任何事情并优化它,并优化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); } 

即使这不是一个有用的testing! 操作系统可能会看到该文件正在被频繁打开,因此可能会将其预先加载到内存中以提高性能。 几乎所有的操作系​​统都这样做。 打开应用程序时也会发生同样的情况 – 操作系统可能会计算出您打开最多的前5个应用程序,并在启动计算机时将应用程序代码预加载到内存中!

实际上,有无数的variables参与其中:引用的地方性(例如数组和链表),caching和内存带宽的影响,编译器内联,编译器实现,编译器开关,处理器内核数量,处理器级别的优化,操作系统调度程序,操作系统后台进程等

所以在许多情况下,微基准标记并不是一个有用的指标。 它绝对不能用定义良好的testing用例(分析)来取代整个程序的基准。 先写可读的代码,然后configuration文件来看看需要做什么,如果有的话。

我想强调的是,微基准本身并不是邪恶的,但必须小心使用它们(对于与计算机相关的许多其他事情也是如此)

微型基准testing没有定义,但是当我使用它时,我的意思是一个小型的人为testing基准,旨在testing某些特定硬件1或语言function的性能。 相比之下,一个更好的基准是一个真正的计划,旨在执行一个真正的任务。 (在这两起案件之间画一条硬线是毫无意义的,国际海事组织,我不会去尝试。)

微观基准testing的危险在于,编写一个基准testing结果是完全误导的,这很容易。 Java微型基准testing中的一些常见陷阱是:

  • 编写编译器可以演绎的代码并不是有用的工作,因此完全可以优化,
  • 没有考虑到Java内存pipe理的“块状”特性
  • 不考虑JVM启动效应; 例如,加载和JIT编译类所花费的时间,以及(相反)一旦方法已经被JIT编译就发生的执行加速。

但是,即使您已经解决了上述问题,基准testing也存在系统性问题,这是不可能解决的。 基准的代码和行为通常与你真正关心的东西没什么关系。 即您的应用程序将如何执行。 有太多的“隐藏variables”可以让你从基准到典型的程序,更不用说你的程序了。

出于这些原因,我们经常build议人们不要浪费时间用微观基准。 相反,最好编写简单而自然的代码,并使用分析器来确定需要手动优化的区域。 有趣的是,通常情况下,实际应用程序中最重要的性能问题是由于数据结构和algorithm(包括networking,数据库和线程相关瓶颈)的糟糕devise,而不是典型的微基准testingtesting。

@BalusC在“ 热点常见问题解答”页面中提供了有关此主题的资料的极好链接。 这里是Brian Goetz的一篇IBM白皮书的链接。


1 – 专家甚至不会尝试在Java中进行硬件基准testing。 在字节码和硬件之间发生太多“复杂的事情”,从原始结果中得出关于硬件的有效/有用的结论。 使用更接近硬件的语言会更好; 例如C或甚至汇编代码。

  • 这是什么意思,什么不是什么意思?

我认为微观基准testing只是测量一些微小的东西。 微小可能是依赖于上下文的,但通常在单个系统调用或类似的级别上。 标杆pipe理是指以上的一切。

  • 什么是IS和IS不是微基准的一些例子?

本文列出了getpid()系统调用的 测量时间,使用memcpy()作为微基准testing的示例来度量复制内存的时间

algorithm实现的任何度量都不会被视为微基准。 尤其是结果报告列出了执行时间缩短的任务可能很less被视为微基准。

  • microbenchmarking有什么危险,你如何避免它?

显而易见的危险是它诱使开发人员优化程序的错误部分。 另外一个危险是难以精确地测量一些小的东西。 避免这种情况的最简单方法可能只是为了了解大部分时间花在程序中的位置。

人们通常会说“不要做微观基准testing”,但是他们可能的意思是“不要根据微观基准做出优化决策”。

  • (或者是件好事?)

本身并不是什么坏事,许多网页似乎都是这样build议的。 它有它的地方。 我使用程序重写和运行时方面编织等。我们通常发布我们添加的指令的微基准,而不是指导任何优化,但确保我们的额外代码几乎不影响重写的程序的执行。

然而,这是一门艺术,尤其是在具有JIT,热身时间等的VM环境中。 这里描述了一个很好描述的Java方法。

以下是Brian Goetz的一些很好的文章,它解释了为什么(微)基准testing在Java中特别困难:

  • dynamic编译和性能测量
  • 一个有缺陷的microbenchmark的解剖
  • 垃圾收集和性能

在“Java性能权威指南”(Java Performance The Definitive Guide)一书中,它具有微观基准的定义和实例

  1. 微基准testing

    微基准是一个testingdevise来测量非常小的单元性能的时间:调用同步方法与非同步方法的时间; 创build一个线程而不是线程池的开销; 执行一个算术algorithm与替代实现; 等等。

    Microbenchmarks似乎是一个好主意,但它们很难正确写入。 consedider下面的代码,这是试图编写一个mocrobenchmark,testing的方法不同执行方法来计算第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)); 

因此,无论Fibonaci方法的执行情况如何,或者应该执行循环的次数,经过的时间将只有几毫秒。

有一个解决这个问题的方法:确保每个结果都被读取,而不是简单的weitten。 实际上,将l的定义从局部variables更改为实例variables(用volatile关键字声明)将允许测量方法的性能。

微基准是我认为不值得的基准。 有效的基准是我认为是值得的基准。

一般来说,microbenchmarking是(正如计算机所说的)试图衡量一些非常细化的任务的性能,这在实际的性能头痛的情况下很难做得很好,通常也是没有意义的。