Являются ли статические вызовы Java более или менее дорогими, чем нестатические вызовы?

95

Есть ли какой-то выигрыш в производительности так или иначе? Это конкретный компилятор / виртуальная машина? Я использую точку доступа.

Энди Файбишенко
источник

Ответы:

74

Во-первых: вам не следует выбирать статический или нестатический на основе производительности.

Второе: на практике никакой разницы не будет. Hotspot может выбрать оптимизацию таким образом, чтобы статические вызовы выполнялись быстрее для одного метода, а нестатические вызовы - для другого.

В-третьих: большая часть мифов, окружающих статическое и нестатическое, основаны либо на очень старых JVM (которые не имели ничего общего с оптимизацией, как у Hotspot), либо на некоторых помнятых мелочах о C ++ (в котором динамический вызов использует еще один доступ к памяти чем статический вызов).

Анон
источник
1
Вы абсолютно правы, не стоит отдавать предпочтение статическим методам, основанным только на этом. Однако в случае, если статические методы хорошо вписываются в дизайн, полезно знать, что они, по крайней мере, так же быстры, если не быстрее, чем методы экземпляра, и не должны исключаться из соображений производительности.
Уилл
2
@AaronDigulla -.- что, если бы я сказал вам, что я пришел сюда, потому что я оптимизирую прямо сейчас, не преждевременно, а когда мне это действительно нужно? Вы предположили, что OP хочет оптимизировать преждевременно, но вы знаете, что этот сайт вроде как глобальный ... Верно? Я не хочу показаться грубым, но, пожалуйста, не допускайте подобных вещей в следующий раз.
Далибор Филус
1
@DaliborFilus Мне нужно найти баланс. Использование статических методов вызывает всевозможные проблемы, поэтому их следует избегать, особенно если вы не знаете, что делаете. Во-вторых, большая часть «медленного» кода возникает из-за (плохого) дизайна, а не потому, что язык по выбору медленный. Если ваш код медленный, статические методы, вероятно, не сохранят его, если только его вызывающие методы не делают абсолютно ничего . В большинстве случаев код в методах затмевает накладные расходы на вызов.
Аарон Дигулла
6
Проголосовали против. Это не отвечает на вопрос. Был задан вопрос о преимуществах производительности. Он не спрашивал мнения о принципах дизайна.
Колм Бхандал
4
Если бы я научил попугая говорить «преждевременная оптимизация - корень всего зла», я бы получил 1000 голосов от людей, которые знают о производительности не меньше, чем попугай.
rghome
62

Четыре года спустя...

Хорошо, в надежде решить этот вопрос раз и навсегда, я написал тест, который показывает, как разные типы вызовов (виртуальные, не виртуальные, статические) сравниваются друг с другом.

Я запустил его на 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указатель не должен быть передан статическим метод. Таким образом, потребовалось бы лишь довольно небольшое количество кода, выполняющего тривиальные вещи между вызовами, чтобы разница между различными типами вызовов уменьшилась до такой степени, что не оказало никакого общего влияния.

Кроме того, вызовы виртуальных методов существуют не просто так; у них действительно есть цель, и они реализуются с использованием наиболее эффективных средств, предоставляемых базовым оборудованием. (Набор инструкций ЦП.) Если, желая устранить их, заменив их невиртуальными или статическими вызовами, вам в конечном итоге придется добавить не меньше йоты дополнительного кода для имитации их функциональности, тогда ваши итоговые чистые накладные расходы будут ограничены быть не меньше, а больше. Вполне возможно, намного, намного, непостижимо много, больше.

Майк Накис
источник
7
«Виртуальный» - это термин C ++. В Java нет виртуальных методов. Существуют обычные методы, полиморфные во время выполнения, и статические или конечные методы, которые не являются.
Женя
16
@levgen: да, для того, чья точка зрения столь же узка, как официальный высокоуровневый обзор языка, это именно то, что вы говорите. Но, конечно, концепции высокого уровня реализуются с использованием хорошо известных механизмов низкого уровня, которые были изобретены задолго до появления Java, и виртуальные методы являются одним из них. Если вы хоть немного загляните под капот, то сразу увидите, что это так: docs.oracle.com/javase/specs/jvms/se7/html/…
Майк Накис
13
Благодарим за ответ на вопрос без предположений о преждевременной оптимизации. Отличный ответ.
vegemite4me
3
Да, именно это я имел в виду. В любом случае я только что провел тест на своей машине. За исключением дрожания, которое можно ожидать от такого теста, нет никакой разницы в скорости: VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198на моей установке OpenJDK. FTR: Это даже правда, если я уберу finalмодификатор. Кстати. Пришлось пройти terminateполе volatile, иначе тест не закончился.
Marten
4
FYI, я получаю довольно неожиданные результаты на Nexus 5 под управлением Android 6: VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. Мало того, что OpenJDK на моем ноутбуке удается выполнить в 40 раз больше итераций, статический тест всегда дает примерно на 30% меньше пропускной способности. Это может быть специфическое явление ART, потому что я получаю ожидаемый результат на планшете Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten
46

Что ж, статические вызовы не могут быть переопределены (поэтому всегда являются кандидатами на встраивание) и не требуют никаких проверок на нулевое значение. HotSpot выполняет кучу классных оптимизаций, например, методов, которые могут свести на нет эти преимущества, но это возможные причины, по которым статический вызов может быть быстрее.

Однако это не должно влиять на ваш дизайн - код наиболее читаемым и естественным образом - и беспокоиться о такого рода микрооптимизации только в том случае, если у вас есть веская причина (чего вы почти никогда не будете).

Джон Скит
источник
Это возможные причины, по которым статический вызов может быть быстрее. Не могли бы вы объяснить мне эти причины?
JavaTechnical
6
@JavaTechnical: ответ объясняет эти причины - без переопределения (что означает, что вам не нужно разрабатывать реализацию для использования каждый раз, и вы можете встроить), и вам не нужно проверять, вызываете ли вы метод на пустая ссылка.
Джон Скит,
6
@JavaTechnical: я не понимаю. Я только что дал вам вещи, которые не нужно вычислять / проверять на статические методы, а также возможность встраивания. Отсутствие работы - это улучшение производительности. Что осталось понять?
Джон Скит,
Статические переменные извлекаются быстрее, чем нестатические?
JavaTechnical
1
@JavaTechnical: Ну, здесь нет проверки на нулевое значение, но если JIT-компилятор может удалить эту проверку (которая будет зависеть от контекста), я бы не ожидал большой разницы. Гораздо важнее такие вещи, как наличие памяти в кеше.
Jon Skeet
18

Это зависит от компилятора / виртуальной машины.

  • Теоретически статический вызов можно сделать немного более эффективным, поскольку он не требует поиска виртуальной функции, а также позволяет избежать накладных расходов, связанных со скрытым параметром this.
  • На практике многие компиляторы все равно оптимизируют это.

Следовательно, вероятно, не стоит беспокоиться, если вы не определили это как действительно критическую проблему производительности в вашем приложении. Преждевременная оптимизация - это корень всех зол и т. Д.

Однако я уже видел это оптимизация дает существенное увеличение производительности в следующей ситуации:

  • Метод, выполняющий очень простые математические вычисления без доступа к памяти
  • Метод вызывается миллионы раз в секунду в тесном внутреннем цикле
  • Приложение с привязкой к ЦП, где важен каждый бит производительности

Если вышесказанное относится к вам, возможно, стоит попробовать.

Есть также еще одна хорошая (и потенциально даже более важная!) Причина использовать статический метод - если метод действительно имеет статическую семантику (т.е. логически не связан с данным экземпляром класса), то имеет смысл сделать его статическим. чтобы отразить этот факт. Тогда опытные Java-программисты заметят модификатор static и сразу же подумают: «Ага! Этот метод статический, поэтому ему не нужен экземпляр и, по-видимому, он не управляет состоянием конкретного экземпляра». Таким образом, вы эффективно передадите статический характер метода ...

Микера
источник
14

Как говорилось на предыдущих плакатах: это похоже на преждевременную оптимизацию.

Однако есть одно отличие (отчасти из-за того, что нестатические вызовы требуют дополнительного проталкивания вызываемого объекта в стек операндов):

Поскольку статические методы нельзя переопределить, во время выполнения не будет никаких виртуальных поисков для вызова статического метода. При некоторых обстоятельствах это может привести к заметной разнице.

Разница на уровне байт-кода является то , что не-статический метод вызова выполняется через INVOKEVIRTUAL, INVOKEINTERFACEили в INVOKESPECIALто время как статический метод вызова выполняется через INVOKESTATIC.

aioobe
источник
2
Однако метод частного экземпляра (по крайней мере обычно) вызывается с использованием, invokespecialпоскольку он не является виртуальным.
Марк Питерс
Ах, интересно, я мог думать только о конструкторах, поэтому и опустил! Спасибо! (обновление ответа)
aioobe
2
JVM выполнит оптимизацию, если будет создан экземпляр одного типа. Если B расширяет A, и экземпляр B не был создан, вызовы методов на A не будут нуждаться в поиске в виртуальной таблице.
Стив Куо,
13

Маловероятно, что какое-либо различие в производительности статических и нестатических вызовов имеет значение для вашего приложения. Помните, что «преждевременная оптимизация - корень всех зол».

DJClayworth
источник
Это был Кнут! shreevatsa.wordpress.com/2008/05/16/…
ShreevatsaR
не могли бы вы объяснить, что означает «преждевременная оптимизация - корень всех зол».?
user2121
Вопрос был в том, «Есть ли какое-то преимущество в производительности так или иначе?», И это как раз и отвечает на этот вопрос.
DJClayworth
13

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);
    }
}
Натан
источник
1
Здесь чисто академический интерес. Мне любопытно узнать о любых потенциальных преимуществах, которые такая ops/sмикрооптимизация может иметь для показателей, отличных от основных в среде ART (например, использование памяти, уменьшенный размер файла .oat и т. Д.). Знаете ли вы какие-либо относительно простые инструменты / способы, которыми можно было бы попытаться сравнить эти другие показатели?
Райан Томас
Hotspot выясняет, что в пути к классам нет расширений для InstanceSum. Попробуйте добавить еще один класс, расширяющий InstanceSum и переопределяющий метод.
Милан
12

Для решения, должен ли метод быть статическим, аспект производительности не должен иметь значения. Если у вас проблемы с производительностью, создание статических методов не спасет положение. Тем не менее, статические методы почти наверняка не медленнее, чем любой метод экземпляра, в большинстве случаев немного быстрее :

1.) Статические методы не являются полиморфными, поэтому JVM должна принимать меньше решений, чтобы найти фактический код для выполнения. Это спорный вопрос в Age of Hotspot, поскольку Hotspot оптимизирует вызовы методов экземпляра, которые имеют только один сайт реализации, поэтому они будут работать одинаково.

2.) Еще одно тонкое отличие состоит в том, что у статических методов явно нет ссылки this. В результате кадр стека на один слот меньше, чем у метода экземпляра с той же сигнатурой и телом ("this" помещается в слот 0 локальных переменных на уровне байт-кода, тогда как для статических методов слот 0 используется для первого параметр метода).

Дюрандаль
источник
5

Может быть разница, и она может повлиять на любой конкретный фрагмент кода, и она может измениться даже с незначительным выпуском JVM.

Это определенно часть из 97% небольших показателей эффективности, о которых вам следует забыть .

Майкл Боргвардт
источник
2
Неправильно. Вы ничего не можете предполагать. Это может быть жесткий цикл, необходимый для интерфейсного интерфейса, который может иметь огромное значение для того, насколько «быстрым» является пользовательский интерфейс. Например, поиск в TableViewмиллионах записей.
трилогия
0

Теоретически дешевле.

Статическая инициализация будет выполняться, даже если вы создаете экземпляр объекта, тогда как статические методы не будут выполнять никакой инициализации, обычно выполняемой в конструкторе.

Однако я этого не проверял.

Пауэрлорд
источник
1
@Р. Бемроуз, какое отношение к этому вопросу имеет статическая инициализация?
Кирк Уолл,
@ Кирк Волл: Поскольку статическая инициализация выполняется при первом обращении к классу ... в том числе до первого вызова статического метода.
Powerlord
@Р. Бемроуз, конечно же, загружает класс в виртуальную машину для начала. Похоже на несеквитор, ИМО.
Кирк Уолл,
0

Как отмечает Джон, статические методы нельзя переопределить, поэтому простой вызов статического метода может быть - в достаточно наивной среде выполнения Java - быстрее, чем вызов метода экземпляра.

Но тогда, даже если предположить, что вы находитесь в той точке, где вы беспокоитесь о том, чтобы испортить свой дизайн, чтобы сэкономить несколько наносекунд, это просто поднимает другой вопрос: понадобится ли вам метод, переопределяющий себя? Если вы измените свой код, чтобы превратить метод экземпляра в статический метод, чтобы сэкономить наносекунду здесь и там, а затем развернитесь и внедрите свой собственный диспетчер поверх этого, ваш почти наверняка будет менее эффективным, чем созданный уже в вашу среду выполнения Java.

Кен
источник
-2

Я хотел бы добавить к другим отличным ответам здесь, что это также зависит от вашего потока, например:

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);
   };
};
Яир Заславский
источник