Есть ли какое-то преимущество в производительности, так или иначе? Это специфично для компилятора/VM? Я использую точку доступа.
Являются ли статические вызовы Java более или менее дорогими, чем нестатические вызовы?
Ответы (12)
Во-первых: вы не должны делать выбор между статическим и нестатическим на основе производительности.
Второе: на практике это не будет иметь никакого значения. Hotspot может выбрать оптимизацию таким образом, чтобы статические вызовы выполнялись быстрее для одного метода, а нестатические вызовы — для другого.
В-третьих: большая часть мифов о статике и нестатичности основана либо на очень старых JVM (которые не делали и близкой оптимизации, которую делает Hotspot), либо на некоторых запомнившихся мелочах о C++ (в которых динамический вызов использует один больший доступ к памяти, чем статический вызов).
Четыре года спустя...
Хорошо, в надежде решить этот вопрос раз и навсегда, я написал тест, который показывает, как различные виды вызовов (виртуальные, невиртуальные, статические) сравниваются друг с другом.
Я запустил его в ideone и вот что получил:
(Чем больше повторений, тем лучше.)
Success time: 3.12 memory: 320576 signal:0
Name | Iterations
VirtualTest | 128009996
NonVirtualTest | 301765679
StaticTest | 352298601
Done.
Как и ожидалось, вызовы виртуальных методов являются самыми медленными, вызовы невиртуальных методов выполняются быстрее, а вызовы статических методов выполняются еще быстрее.
Чего я не ожидал, так это того, что различия будут столь заметными: было измерено, что вызовы виртуальных методов выполняются со скоростью менее половины скорости вызовов невиртуальных методов, которые, в свою очередь, выполнялись в целом < strong>на 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
. Таким образом, потребовалось бы совсем небольшое количество кода, выполняющего тривиальные действия между вызовами, чтобы разница между различными типами вызовов была размыта до такой степени, что не имела бы никакого общего влияния.
Кроме того, не просто так существуют вызовы виртуальных методов; у них действительно есть цель, и они реализованы с использованием наиболее эффективных средств, предоставляемых базовым оборудованием. (Набор инструкций ЦП.) Если в вашем желании избавиться от них, заменив их невиртуальными или статическими вызовами, вам в конечном итоге придется добавить хоть йоту дополнительного кода для эмуляции их функциональности, то ваши результирующие чистые накладные расходы ограничены. быть не меньше, а больше. Вполне возможно, намного, намного, непостижимо намного, больше.
Runnable
, является окончательным, не должно быть причин разрешать виртуальный метод более одного раза. Или я что-то упускаю?
- person Marten; 14.09.2016
VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198
на моей установке OpenJDK. FTR: Это верно, даже если я уберу модификатор final
. Кстати. Пришлось сделать поле terminate
volatile
, иначе тест не закончился.
- person Marten; 17.09.2016
VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170
. Мало того, что OpenJDK на моем ноутбуке выполняет в 40 раз больше итераций, статический тест всегда имеет примерно на 30% меньшую пропускную способность. Это может быть специфическим феноменом ART, потому что я получаю ожидаемый результат на планшете с Android 4.4: VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
- person Marten; 17.09.2016
VirtualTest | 13818374 -- NonVirtualTest | 14226863 -- StaticTest | 16138893
, так что только 10% исходных чисел. Я увеличил DURATION
в 10 раз, прежде чем запускать этот тест. Все еще не медленнее, чем Nexus 5, что странно.
- person Marten; 17.09.2016
VirtualTest = 542M; NonVirtualTest = 607M; StaticTest = 684M
- person Mike Nakis; 11.10.2016
VirtualTest = 631M; NonVirtualTest = 629M; StaticTest = 689M
. (Таким образом, нет разницы между виртуальным и невиртуальным и менее выраженной разницы между теми и статическими.)
- person Mike Nakis; 11.10.2016
terminate
как volatile
в некоторых средах, заключается в том, что он выполняется в холодном состоянии, т. е. затем интерпретируется, что делает весь «тест» спорным. На практике любое место кода, критичное для производительности, будет оптимизировано, что не только устранит накладные расходы на вызов метода, но и избыточное чтение энергонезависимой переменной. В моей системе воспроизводимые зависания даже без прогрева. Конечно, добавление к флагу модификатора volatile
сделало бы и весь тест бесполезным. Сделать осмысленный микротест не так уж и просто.
- person Holger; 28.01.2019
Итак, статические вызовы нельзя переопределить (поэтому они всегда являются кандидатами на встраивание) и не требуют проверки на недействительность. HotSpot делает множество классных оптимизаций для методов экземпляра, которые вполне могут свести на нет эти преимущества, но они являются возможными причинами того, что статический вызов может быть быстрее.
Тем не менее, это не должно влиять на ваш дизайн — код должен быть максимально читабельным и естественным образом — и беспокоиться о такого рода микрооптимизации можно только в том случае, если у вас есть веская причина (чего вы почти никогда не будете).
Это зависит от компилятора/VM.
- Теоретически статический вызов можно сделать несколько более эффективным, поскольку ему не нужно выполнять поиск виртуальной функции, а также можно избежать накладных расходов, связанных со скрытым параметром this.
- На практике многие компиляторы все равно оптимизируют это.
Следовательно, об этом, вероятно, не стоит беспокоиться, если вы не определили это как действительно критическую проблему производительности в вашем приложении. Преждевременная оптимизация - корень всех зол и т.д...
Однако я увидел, что эта оптимизация значительно повысила производительность в следующей ситуации:
- Метод, выполняющий очень простой математический расчет без обращения к памяти.
- Метод вызывается миллионы раз в секунду в тесном внутреннем цикле
- Приложение с привязкой к процессору, где важен каждый бит производительности
Если вышесказанное относится к вам, возможно, стоит проверить.
Есть еще одна веская (и потенциально даже более важная!) причина для использования статического метода - если метод на самом деле имеет статическую семантику (т.е. логически не связан с данным экземпляром класса), то имеет смысл сделать его статическим отразить этот факт. Опытные Java-программисты заметят модификатор static и сразу же подумают: «Ага! Этот метод статический, поэтому ему не нужен экземпляр и, по-видимому, он не манипулирует конкретным состоянием экземпляра». Таким образом, вы эффективно сообщили о статической природе метода....
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);
}
}
ops/s
, прежде всего в среде ART (например, использование памяти, уменьшенный размер файла .oat и т. д.). Знаете ли вы какие-либо относительно простые инструменты/способы, с помощью которых можно было бы попытаться сравнить эти другие показатели?
- person Ryan Thomas; 05.10.2019
Как уже говорилось в предыдущих постах: это похоже на преждевременную оптимизацию.
Однако есть одно отличие (частично из-за того, что нестатические вызовы требуют дополнительного помещения вызываемого объекта в стек операндов):
Поскольку статические методы не могут быть переопределены, не будет никакого виртуального поиска во время выполнения для вызова статического метода. Это может привести к заметной разнице при некоторых обстоятельствах.
Разница на уровне байт-кода заключается в том, что вызов нестатического метода выполняется через INVOKEVIRTUAL
, INVOKEINTERFACE
или INVOKESPECIAL
, в то время как вызов статического метода выполняется через INVOKESTATIC
.
invokespecial
, поскольку он не является виртуальным.
- person Mark Peters; 27.09.2010
Невероятно маловероятно, что какая-либо разница в производительности статических и нестатических вызовов влияет на ваше приложение. Помните, что «преждевременная оптимизация — корень всех зол».
Для решения, должен ли метод быть статическим, аспект производительности не должен иметь значения. Если у вас есть проблемы с производительностью, то статическое множество методов не спасет ситуацию. Тем не менее, статические методы почти наверняка не медленнее, чем любой метод экземпляра, в большинстве случаев немного быстрее:
1.) Статические методы не полиморфны, поэтому у JVM меньше решений, чтобы найти фактический код для выполнения. Это спорный вопрос в эпоху Hotspot, поскольку Hotspot будет оптимизировать вызовы методов экземпляра, которые имеют только один сайт реализации, поэтому они будут работать одинаково.
2.) Еще одно тонкое отличие состоит в том, что статические методы явно не имеют ссылки на this. Это приводит к тому, что кадр стека на один слот меньше, чем у метода экземпляра с той же сигнатурой и телом («это» помещается в слот 0 локальных переменных на уровне байт-кода, тогда как для статических методов слот 0 используется для первого параметр метода).
Может быть разница, и она может быть в любом случае для любого конкретного фрагмента кода, и она может измениться даже с второстепенным выпуском JVM.
Это определенно часть 97 % небольших преимуществ, о которых вам следует забыть.
TableView
миллионов записей.
- person trilogy; 05.12.2018
Теоретически дешевле.
Статическая инициализация будет выполнена, даже если вы создадите экземпляр объекта, тогда как статические методы не будут выполнять никакой инициализации, обычно выполняемой в конструкторе.
Однако я не проверял это.
Как отмечает Джон, статические методы нельзя переопределить, поэтому простой вызов статического метода может оказаться — в достаточно наивной среде выполнения 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);
};
};