【发布时间】:2011-04-17 20:04:58
【问题描述】:
以一种或另一种方式有任何性能优势吗?它是编译器/虚拟机特定的吗?我正在使用热点。
【问题讨论】:
标签: java performance premature-optimization
以一种或另一种方式有任何性能优势吗?它是编译器/虚拟机特定的吗?我正在使用热点。
【问题讨论】:
标签: java performance premature-optimization
7 年后...
我对 Mike Nakis 发现的结果没有很大的信心,因为他们没有解决与热点优化相关的一些常见问题。我使用 JMH 对基准进行了检测,发现实例方法的开销在我的机器上与静态调用相比约为 0.75%。考虑到低开销,我认为除了对延迟最敏感的操作之外,它可以说不是应用程序设计中最大的问题。我的 JMH 基准测试的总结结果如下:
java -jar target/benchmark.jar
# -- snip --
Benchmark Mode Cnt Score Error Units
MyBenchmark.testInstanceMethod thrpt 200 414036562.933 ± 2198178.163 ops/s
MyBenchmark.testStaticMethod thrpt 200 417194553.496 ± 1055872.594 ops/s
你可以在 Github 上看这里的代码;
https://github.com/nfisher/svsi
基准测试本身非常简单,但旨在最大限度地减少死代码消除和常量折叠。我可能错过/忽略了其他优化,这些结果可能因 JVM 版本和操作系统而异。
package ca.junctionbox.svsi;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;
class InstanceSum {
public int sum(final int a, final int b) {
return a + b;
}
}
class StaticSum {
public static int sum(final int a, final int b) {
return a + b;
}
}
public class MyBenchmark {
private static final InstanceSum impl = new InstanceSum();
@State(Scope.Thread)
public static class Input {
public int a = 1;
public int b = 2;
}
@Benchmark
public void testStaticMethod(Input i, Blackhole blackhole) {
int sum = StaticSum.sum(i.a, i.b);
blackhole.consume(sum);
}
@Benchmark
public void testInstanceMethod(Input i, Blackhole blackhole) {
int sum = impl.sum(i.a, i.b);
blackhole.consume(sum);
}
}
【讨论】:
ops/s 以外的指标产生的任何潜在好处,主要是在 ART 环境中(例如内存使用、减少 .oat 文件大小等)。您是否知道可以尝试对这些其他指标进行基准测试的任何相对简单的工具/方法?
四年后...
好的,为了一劳永逸地解决这个问题,我编写了一个基准来显示不同类型的调用(虚拟、非虚拟、静态)之间的比较。
我运行它on ideone,这就是我得到的:
(迭代次数越多越好。)
Success time: 3.12 memory: 320576 signal:0
Name | Iterations
VirtualTest | 128009996
NonVirtualTest | 301765679
StaticTest | 352298601
Done.
正如预期的那样,虚拟方法调用最慢,非虚拟方法调用更快,静态方法调用甚至更快。
我没想到的是差异如此明显:虚拟方法调用的运行速度不到非虚拟方法调用速度的一半,而非虚拟方法调用的速度又被测量为运行比静态调用慢 15%。这就是这些测量所显示的;实际上,实际差异必须更明显一些,因为对于每个虚拟、非虚拟和静态方法调用,我的基准测试代码都有额外的常量开销,即递增一个整数变量、检查一个布尔变量,如果不正确则循环。
我想结果会因 CPU 和 JVM 不同而不同,因此请尝试一下,看看你会得到什么:
import java.io.*;
class StaticVsInstanceBenchmark
{
public static void main( String[] args ) throws Exception
{
StaticVsInstanceBenchmark program = new StaticVsInstanceBenchmark();
program.run();
}
static final int DURATION = 1000;
public void run() throws Exception
{
doBenchmark( new VirtualTest( new ClassWithVirtualMethod() ),
new NonVirtualTest( new ClassWithNonVirtualMethod() ),
new StaticTest() );
}
void doBenchmark( Test... tests ) throws Exception
{
System.out.println( " Name | Iterations" );
doBenchmark2( devNull, 1, tests ); //warmup
doBenchmark2( System.out, DURATION, tests );
System.out.println( "Done." );
}
void doBenchmark2( PrintStream printStream, int duration, Test[] tests ) throws Exception
{
for( Test test : tests )
{
long iterations = runTest( duration, test );
printStream.printf( "%15s | %10d\n", test.getClass().getSimpleName(), iterations );
}
}
long runTest( int duration, Test test ) throws Exception
{
test.terminate = false;
test.count = 0;
Thread thread = new Thread( test );
thread.start();
Thread.sleep( duration );
test.terminate = true;
thread.join();
return test.count;
}
static abstract class Test implements Runnable
{
boolean terminate = false;
long count = 0;
}
static class ClassWithStaticStuff
{
static int staticDummy;
static void staticMethod() { staticDummy++; }
}
static class StaticTest extends Test
{
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
ClassWithStaticStuff.staticMethod();
}
}
}
static class ClassWithVirtualMethod implements Runnable
{
int instanceDummy;
@Override public void run() { instanceDummy++; }
}
static class VirtualTest extends Test
{
final Runnable runnable;
VirtualTest( Runnable runnable )
{
this.runnable = runnable;
}
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
runnable.run();
}
}
}
static class ClassWithNonVirtualMethod
{
int instanceDummy;
final void nonVirtualMethod() { instanceDummy++; }
}
static class NonVirtualTest extends Test
{
final ClassWithNonVirtualMethod objectWithNonVirtualMethod;
NonVirtualTest( ClassWithNonVirtualMethod objectWithNonVirtualMethod )
{
this.objectWithNonVirtualMethod = objectWithNonVirtualMethod;
}
@Override
public void run()
{
for( count = 0; !terminate; count++ )
{
objectWithNonVirtualMethod.nonVirtualMethod();
}
}
}
static final PrintStream devNull = new PrintStream( new OutputStream()
{
public void write(int b) {}
} );
}
值得注意的是,这种性能差异仅适用于除了调用无参数方法之外什么都不做的代码。调用之间的任何其他代码都会淡化差异,这包括参数传递。实际上,静态调用和非虚拟调用之间 15% 的差异可能完整通过 this 指针不必传递给静态方法这一事实来解释。因此,只需在调用之间执行少量代码就可以将不同类型调用之间的差异稀释到没有任何净影响的程度。
另外,虚方法调用的存在是有原因的;它们确实有服务的目的,并且它们是使用底层硬件提供的最有效的方式实现的。 (CPU 指令集。)如果您希望通过用非虚拟或静态调用替换它们来消除它们,最终不得不添加尽可能多的额外代码来模拟它们的功能,那么您产生的净开销是有限的不是更少,而是更多。很可能,很多,很多,深不可测的很多,更多。
【讨论】:
VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198。 FTR:即使我删除了final 修饰符,情况也是如此。顺便提一句。我必须将terminate 字段设为volatile,否则测试没有完成。
VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170。不仅我笔记本上的 OpenJDK 能够执行 40 倍以上的迭代,静态测试的吞吐量也总是降低 30% 左右。这可能是 ART 特有的现象,因为我在 Android 4.4 平板电脑上得到了预期的结果:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
我想在这里添加其他很棒的答案,这也取决于您的流程,例如:
Public class MyDao {
private String sql = "select * from MY_ITEM";
public List<MyItem> getAllItems() {
springJdbcTemplate.query(sql, new MyRowMapper());
};
};
请注意,每次调用都会创建一个新的 MyRowMapper 对象。
相反,我建议在这里使用静态字段。
Public class MyDao {
private static RowMapper myRowMapper = new MyRowMapper();
private String sql = "select * from MY_ITEM";
public List<MyItem> getAllItems() {
springJdbcTemplate.query(sql, myRowMapper);
};
};
【讨论】:
正如之前的海报所说:这似乎是一个过早的优化。
但是,有一个区别(部分原因是非静态调用需要将被调用对象额外推送到操作数堆栈):
由于静态方法不能被覆盖,对于静态方法调用,在运行时不会有任何虚拟查找。在某些情况下,这可能会导致明显的差异。
字节码级别的区别在于,非静态方法调用通过INVOKEVIRTUAL、INVOKEINTERFACE或INVOKESPECIAL完成,而静态方法调用通过INVOKESTATIC完成。
【讨论】:
invokespecial 调用,因为它不是虚拟的。
对于决定一个方法是否应该是静态的,性能方面应该是无关紧要的。如果您遇到性能问题,那么将许多方法设为静态并不能化险为夷。也就是说,静态方法几乎肯定不会比任何实例方法慢,在大多数情况下略快:
1.) 静态方法不是多态的,因此 JVM 在查找要执行的实际代码时做出的决定较少。这是 Hotspot 时代的一个争论点,因为 Hotspot 将优化只有一个实现站点的实例方法调用,因此它们将执行相同的操作。
2.) 另一个细微的区别是静态方法显然没有“this”引用。这导致堆栈帧比具有相同签名和主体的实例方法的插槽小一个插槽(“this”放在字节码级别的局部变量的插槽 0 中,而对于静态方法,插槽 0 用于第一个方法的参数)。
【讨论】:
它是编译器/VM 特定的。
因此,除非您已将其确定为应用程序中真正关键的性能问题,否则可能不值得为此烦恼。过早的优化是万恶之源……
但是我已经看到这种优化在以下情况下可以显着提高性能:
如果上述情况适用于您,则可能值得测试。
使用静态方法还有另一个好的(甚至可能更重要!)理由 - 如果该方法实际上具有静态语义(即逻辑上未连接到类的给定实例),那么它是有意义的使其静态以反映这一事实。有经验的 Java 程序员然后会注意到 static 修饰符并立即认为“啊哈!这个方法是静态的,所以它不需要实例并且可能不会操纵实例特定的状态”。因此,您将有效地传达该方法的静态性质....
【讨论】:
正如 Jon 所说,静态方法不能被覆盖,因此简单地调用静态方法可能——在足够幼稚的 Java 运行时——比调用更快一个实例方法。
但是,即使假设您正在考虑搞乱您的设计以节省几纳秒,这也带来了另一个问题:您是否需要覆盖自己的方法?如果您更改代码以将实例方法变为静态方法以在这里和那里节省一纳秒,然后转身并在此基础上实现您自己的调度程序,那么您的调度程序几乎肯定会比构建的效率低已经进入您的 Java 运行时。
【讨论】:
可能存在差异,对于任何特定的代码段都可能存在差异,甚至可能会随着 JVM 的次要版本而改变。
这绝对是the 97% of small efficiencies that you should forget about 的一部分。
【讨论】:
TableView 中搜索数百万条记录。
好吧,静态调用不能被覆盖(内联的候选者总是如此),并且不需要任何无效性检查。 HotSpot 对实例方法进行了一系列很酷的优化,这些优化很可能会抵消这些优势,但它们是可能静态调用可能更快的原因。
但是,这不应该影响您的设计 - 以最易读、最自然的方式编写代码 - 并且只有在有正当理由时才担心这种微优化(您几乎永远不会 )。
【讨论】:
首先:您不应该根据性能来选择静态还是非静态。
第二:在实践中,它不会有任何区别。 Hotspot 可能会选择以使一种方法的静态调用更快、另一种方法的非静态调用更快的方式进行优化。
第三:围绕静态与非静态的许多神话要么基于非常古老的 JVM(它没有做任何接近 Hotspot 所做的优化),要么是一些记忆中的关于 C++ 的琐事(其中动态调用使用 比静态调用多一个内存访问)。
【讨论】:
理论上,更便宜。
即使你创建了一个对象的实例,也会进行静态初始化,而静态方法通常不会在构造函数中进行任何初始化。
但是,我没有对此进行测试。
【讨论】:
令人难以置信的是,静态调用与非静态调用的任何性能差异都不会对您的应用程序产生影响。请记住“过早的优化是万恶之源”。
【讨论】: