pol*_*nts 58 java optimization performance benchmarking jit
我听说过这个术语,但我不完全确定它是什么意思,所以:
In *_*ico 70
它完全意味着它在锡罐上所说的 - 它测量的是"小"的性能,就像系统调用操作系统的内核一样.
危险在于人们可能会使用从微基准测试中获得的任何结果来指示优化.众所周知:
我们应该忘记小的效率,大约97%的时间说:过早的优化是所有邪恶的根源" - 唐纳德·克努特
可能有许多因素会扭曲微基准测试的结果.编译器优化就是其中之一.如果正在测量的操作花费的时间太少,以至于无论您使用什么测量它都需要比实际操作本身更长的时间,那么您的微基准测试也会受到影响.
例如,有人可能会采用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);
}
Run Code Online (Sandbox Code Playgroud)
显然,编译器可以看到循环完全没有任何东西,并且根本不会为循环生成任何代码.因此,elapsed
和的价值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);
}
Run Code Online (Sandbox Code Playgroud)
编译器可能会看到变量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
}
Run Code Online (Sandbox Code Playgroud)
编译器可能足够聪明,可以实现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);
}
Run Code Online (Sandbox Code Playgroud)
即使这不是一个有用的测试!操作系统可能会看到文件被频繁打开,因此可能会将其预加载到内存中以提高性能.几乎所有操作系统都这样做.打开应用程序时会发生同样的事情 - 操作系统可能会找出打开最多的前5个应用程序,并在启动计算机时将应用程序代码预加载到内存中!
实际上,有无数变量发挥作用:引用的位置(例如数组与链表),缓存和内存带宽的影响,编译器内联,编译器实现,编译器开关,处理器内核数量,处理器级别的优化,操作系统调度程序,操作系统后台进程等
因此,在许多情况下,微基准测试并不是一个有用的指标.它肯定不会用明确定义的测试用例(分析)取代整个程序基准.首先编写可读代码,然后编写配置文件以查看需要执行的操作(如果有).
我想强调的是,microbenchmarks 本身并不是邪恶的,但是必须谨慎使用它们(对于许多与计算机相关的其他事情都是如此)
没有微基准测试的定义,但是当我使用它时,我的意思是一个小的人工基准测试,用于测试某些特定硬件1或语言功能的性能.相比之下,更好的基准是一个真正的程序,旨在执行一项真正的任务.(在两个案例之间划清界限是毫无意义的,IMO,我不会尝试.)
微基准测试的危险在于,编写一个基准测试很容易产生完全误导的结果.Java微基准测试中的一些常见陷阱是:
但是,即使您已经解决了上述问题,也存在无法解决基准测试的系统性问题.基准的代码和行为通常与您真正关心的内容没有多大关系; 即你的应用程序将如何执行.有太多的"隐藏变量"可供您从基准测试推广到典型程序,更不用说您的程序了.
出于这些原因,我们经常建议人们不要浪费时间使用微基准测试.相反,最好编写简单而自然的代码,并使用分析器来识别需要手动优化的区域.有趣的是,它通常原来,在实际应用中最显著的性能问题都是由于数据结构和算法的糟糕的设计(包括网络,数据库和线程相关的瓶颈),而不是典型的微基准测试试图样的东西测试.
@BalusC在Hotspot FAQ页面中提供了有关此主题材料的出色链接.以下是Brian Goetz撰写的IBM白皮书链接.
1 - 专家甚至不会尝试在Java中进行硬件基准测试.在字节码和硬件之间发生了太多"复杂的事情",从原始结果中得出有关硬件的有效/有用的结论.你最好使用更接近硬件的语言; 例如C甚至汇编代码.
- 它意味着什么,不意味着什么?
我想说,微基准测试只是意味着测量微小的东西。Tiny 可能依赖于上下文,但通常依赖于单个系统调用或类似的级别。基准测试指的是以上所有内容。
- 有哪些例子可以说明什么是微基准测试,什么不是微基准测试?
这篇(已存档)文章列出了测量 getpid() 系统调用的时间和测量使用 memcpy() 复制内存的时间作为微基准测试的示例。
对算法实现等的任何测量都不算作微基准测试。特别是列出执行时间减少的任务的结果报告可能很少被视为微基准测试。
- 微基准测试有哪些危险以及如何避免?
明显的危险是它会诱使开发人员优化程序的错误部分。另一个危险是,准确测量小物体是非常困难的。避免这种情况的最简单方法可能就是清楚地了解程序中花费最多时间的地方。
人们通常说“不要进行微基准测试”,但他们的意思可能是“不要根据微基准做出优化决策”。
- (或者这是一件好事吗?)
这本身并不像这里的其他人那样是一件坏事,而且许多网页似乎都表明了这一点。它有它的地方。我从事程序重写和运行时方面编织等工作。我们通常会发布添加指令的微基准,不是为了指导任何优化,而是确保我们的额外代码对重写程序的执行几乎没有影响。
然而,这是一门艺术,尤其是在具有 JIT、预热时间等的 VM 环境中。此处描述了一种详细描述的 Java 方法(已存档)。
小智 5
《Java Performance: The Definitive Guide》一书中有关于微基准的定义和示例:
微基准测试
微基准测试是一种旨在测量非常小的单元性能的测试:调用同步方法与非同步方法的时间;创建线程与使用线程池的开销;执行一种算术算法与替代实现的时间;等等。
微基准似乎是个好主意,但正确编写它们非常困难。考虑下面的代码,它试图编写一个微基准测试来测试计算第 50 个斐波那契数的方法的不同实现的性能:
Run Code Online (Sandbox Code Playgroud)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 编译器)最终将执行以下代码:
Run Code Online (Sandbox Code Playgroud)long then = System.currentTimeMillis(); long now = System.currentTimeMillis(); System.out.println("Elapsed time: " + (now - then));
因此,无论斐波那契方法的实现如何,或者循环应该执行多少次,所花费的时间都将只有几毫秒。
有一种方法可以解决这个特定问题:确保每个结果都被读取,而不是简单地写入。实际上,将 l 的定义从局部变量更改为实例变量(使用 volatile 关键字声明)将允许测量该方法的性能。