Являются ли статические вызовы Java более или менее дорогими, чем нестатические вызовы?
Есть ли какой-либо выигрыш в производительности, так или иначе? Это зависит от компилятора / виртуальной машины? Я использую Hotspot.
12 ответов
Во-первых, вы не должны делать выбор между статическим и нестатическим, исходя из производительности.
Второе: на практике это не имеет никакого значения. Hotspot может выбрать оптимизацию таким образом, чтобы сделать статические вызовы быстрее для одного метода, нестатические вызовы быстрее для другого.
Третье: большая часть мифов, окружающих статические и нестатические, основана либо на очень старых JVM (которые не имели ничего общего с оптимизацией, которую выполняет Hotspot), либо на некоторых запоминающихся мелочах о C++ (в которых динамический вызов использует еще один доступ к памяти чем статический вызов).
Четыре года спустя...
Хорошо, в надежде решить этот вопрос раз и навсегда, я написал тест, который показывает, как различные виды вызовов (виртуальные, не виртуальные, статические) сравниваются друг с другом.
Я запустил его на ideone, и вот что я получил:
(Чем больше число итераций, тем лучше.)
Success time: 3.12 memory: 320576 signal:0
Name | Iterations
VirtualTest | 128009996
NonVirtualTest | 301765679
StaticTest | 352298601
Done.
Как и ожидалось, вызовы виртуальных методов - самые медленные, вызовы невиртуальных методов - быстрее, а вызовы статических методов - еще быстрее.
Чего я не ожидал, так это того, что различия будут настолько явными: измеренные вызовы виртуальных методов выполнялись со скоростью менее половины скорости вызовов не виртуальных методов, которые, в свою очередь, измерялись на 15% медленнее, чем статические. Это то, что показывают эти измерения; фактические различия на самом деле должны быть чуть более выраженными, поскольку для каждого вызова виртуального, не виртуального и статического методов мой код для сравнительного анализа имеет дополнительные постоянные накладные расходы, заключающиеся в увеличении одной целочисленной переменной, проверке логической переменной и циклическом выполнении, если оно не истинно.
Я предполагаю, что результаты будут варьироваться от процессора к процессору и от JVM до 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
указатель не должен быть передан статическому методу. Таким образом, потребовалось бы довольно небольшое количество кода, выполняющего тривиальные вещи между вызовами, чтобы разница между различными типами вызовов была уменьшена до такой степени, чтобы не иметь никакого влияния на сеть вообще.
Кроме того, виртуальные вызовы методов существуют по причине; у них есть цель служить, и они реализуются с использованием наиболее эффективных средств, предоставляемых базовым оборудованием. (Набор инструкций ЦП.) Если, если вы хотите устранить их, заменив их не виртуальными или статическими вызовами, вам в конечном итоге потребуется добавить еще йоту дополнительного кода для эмуляции их функциональных возможностей, тогда ваши чистые накладные расходы будут связаны быть не меньше, а больше. Вполне возможно, много, много, невообразимо много, больше.
Ну, статические вызовы не могут быть переопределены (поэтому всегда кандидаты для встраивания), и не требуют каких-либо проверок недействительности. HotSpot выполняет кучу классных оптимизаций для методов экземпляров, которые могут свести на нет эти преимущества, но они являются возможными причинами, по которым статический вызов может быть быстрее.
Однако это не должно влиять на ваш дизайн - код наиболее читаемым, естественным образом - и беспокоиться только о такой микрооптимизации, если у вас есть только одна причина (которой вы почти никогда не будете).
Это зависит от компилятора / виртуальной машины.
- Теоретически статический вызов можно сделать несколько более эффективным, поскольку он не требует поиска виртуальной функции, а также может избежать накладных расходов скрытого параметра "this".
- На практике многие компиляторы все равно оптимизируют это.
Следовательно, это не стоит беспокоиться, если вы не определили это как действительно критическую проблему производительности в вашем приложении. Преждевременная оптимизация - корень всего зла и т. Д.
Однако я видел, что эта оптимизация дает существенное увеличение производительности в следующей ситуации:
- Метод выполнения очень простого математического вычисления без доступа к памяти
- Метод вызывается миллионы раз в секунду в тесном внутреннем цикле
- Приложение с привязкой к процессору, где важен каждый бит производительности
Если вышеизложенное относится к вам, возможно, стоит попробовать.
Есть и еще одна веская (и, возможно, даже более важная!) Причина использовать статический метод - если метод на самом деле имеет статическую семантику (т. Е. Логически не связан с данным экземпляром класса), то имеет смысл сделать его статическим чтобы отразить этот факт. Затем опытные Java-программисты заметят статический модификатор и сразу подумают: "Ага! Этот метод статичен, поэтому ему не нужен экземпляр и, по-видимому, он не манипулирует конкретным состоянием экземпляра". Таким образом, вы будете эффективно передавать статическую природу метода....
7 лет спустя...
Я не очень уверен в результатах, которые нашел Майк Накис, потому что они не решают некоторые общие проблемы, связанные с оптимизацией Hotspot. Я тестировал тесты с использованием 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);
}
}
Как говорилось в предыдущих постерах: это кажется преждевременной оптимизацией.
Однако есть одно отличие (от того факта, что нестатические вызовы требуют дополнительного перемещения объекта вызываемого в стек операндов):
Поскольку статические методы не могут быть переопределены, во время выполнения не будет никаких виртуальных поисков для вызова статического метода. Это может привести к заметной разнице при некоторых обстоятельствах.
Разница на уровне байт-кода заключается в том, что вызов нестатического метода выполняется через INVOKEVIRTUAL
, INVOKEINTERFACE
или же INVOKESPECIAL
в то время как статический вызов метода выполняется через INVOKESTATIC
,
Для решения о том, должен ли метод быть статичным, аспект производительности должен быть неактуальным. Если у вас есть проблемы с производительностью, статичные методы не спасут день. Тем не менее, статические методы почти наверняка не медленнее, чем любой метод экземпляра, в большинстве случаев незначительно быстрее:
1.) Статические методы не являются полиморфными, поэтому у JVM меньше решений, чтобы найти реальный код для выполнения. Это спорный момент в Age of Hotspot, поскольку Hotspot оптимизирует вызовы методов экземпляра, которые имеют только один сайт реализации, поэтому они будут выполнять то же самое.
2.) Другое тонкое отличие состоит в том, что статические методы, очевидно, не имеют ссылки "this". Это приводит к тому, что кадр стека на один слот меньше, чем у метода экземпляра с той же сигнатурой и телом ("this" помещается в слот 0 локальных переменных на уровне байт-кода, тогда как для статических методов слот 0 используется для первого параметр метода).
Невероятно маловероятно, что какое-либо различие в производительности статических и нестатических вызовов имеет значение в вашем приложении. Помните, что "преждевременная оптимизация - корень всего зла".
Может быть разница, и она может пойти в любую сторону для любого конкретного фрагмента кода, и она может измениться даже с небольшим выпуском JVM.
Это наиболее определенная часть 97% малых эффективности, о которых вы должны забыть.
В теории дешевле.
Статическая инициализация будет выполняться, даже если вы создадите экземпляр объекта, тогда как статические методы не будут выполнять никакой инициализации, обычно выполняемой в конструкторе.
Тем не менее, я не проверял это.
Как отмечает Джон, статические методы не могут быть переопределены, поэтому простой вызов статического метода может быть - на достаточно наивной среде выполнения Java - быстрее, чем вызов метода экземпляра.
Но тогда, даже если предположить, что вы находитесь в точке, где вам нужно испортить свой дизайн, чтобы сэкономить несколько наносекунд, это просто поднимает другой вопрос: вам нужен метод, переопределяющий себя? Если вы измените свой код так, чтобы превратить метод экземпляра в статический метод, чтобы сохранить наносекунду здесь и там, а затем развернуться и реализовать свой собственный диспетчер, то ваш почти наверняка будет менее эффективным, чем построенный в вашу среду выполнения Java уже.
Я хотел бы добавить к другим отличным ответам здесь, что это также зависит от вашего потока, например:
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);
};
};