Чи статичні дзвінки Java більш чи менш дорогі, ніж нестатичні дзвінки?


Відповіді:


74

По-перше: вам не слід робити вибір статичних проти нестатичних на основі продуктивності.

Друге: на практиці це не призведе до різниці. Точка доступу може вибрати оптимізацію способами, які роблять статичні дзвінки швидшими для одного методу, а нестатичні дзвінки - швидшими для іншого.

По-третє: велика частина міфів, що оточують статичний та нестатичний, базуються або на дуже старих 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, інакше тест не закінчився.
Мартен

4
FYI, я отримую досить несподівані результати на Nexus 5 під управлінням Android 6: VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. Мало того, що OpenJDK на моєму ноутбуці вдається виконати на 40 разів більше ітерацій, статичний тест завжди має приблизно 30% менше пропускної здатності. Це може бути специфічним для АРТ явищем, адже я отримую очікуваний результат на планшеті Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten

46

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

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


Вони можливі, чому статичний дзвінок може бути швидшим. Чи можете ви пояснити мені ці причини?
JavaTechnical

6
@JavaTechnical: Відповідь пояснює ці причини - відсутність перевизначення (це означає, що вам не потрібно розробляти реалізацію, щоб кожен раз використовувати, і ви можете вбудувати), і вам не потрібно перевіряти, чи викликаєте ви метод на нульове посилання.
Джон Скіт

6
@JavaTechnical: Я не розумію. Я щойно дав вам речі, які не потрібно обчислювати / перевіряти на статичні методи, а також можливість вбудовування. Не виконувати роботу - це вигода для продуктивності. Що залишається зрозуміти?
Джон Скіт

Чи отримуються статичні змінні швидше, ніж нестатичні змінні?
JavaTechnical

1
@JavaTechnical: Ну, немає ніякої перевірки недійсності, але якщо компілятор JIT зможе зняти цю перевірку (що буде залежно від контексту), я не очікував би великої різниці. Такі речі, як пам'ять у кеш-пам’яті, були б набагато важливішими.
Джон Скіт,

18

Це спеціально для компілятора / віртуальної машини.

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

Тому, мабуть, не варто турбуватися, якщо ви не визначили це як справді критичну проблему продуктивності у своєму додатку. Передчасна оптимізація - корінь усього зла тощо ...

Однак я вже бачив це оптимізація дає істотне збільшення продуктивності в такій ситуації:

  • Метод виконання дуже простого математичного обчислення без доступу до пам'яті
  • Метод викликається мільйони разів на секунду в тісному внутрішньому циклі
  • Додаток, пов’язаний із процесором, де кожен біт продуктивності мав значення

Якщо вищезазначене стосується вас, можливо, варто перевірити.

Існує також ще одна хороша (і потенційно навіть важливіша!) Причина використання статичного методу - якщо метод насправді має статичну семантику (тобто логічно не пов'язаний із заданим екземпляром класу), то має сенс зробити його статичним щоб відобразити цей факт. Потім досвідчені програмісти Java помітять статичний модифікатор і відразу подумають "ага! Цей метод є статичним, тому йому не потрібен екземпляр і, імовірно, не маніпулює конкретним станом екземпляра". Отже, ви ефективно передасте статичну природу методу ....


14

Як говорили попередні плакати: Це здається передчасною оптимізацією.

Однак є одна відмінність (частина від того, що нестатичні виклики вимагають додаткового натискання викличеного об'єкта на стек операндів):

Оскільки статичні методи неможливо замінити, віртуальних пошуків під час виконання статичного виклику методу не буде. Це може призвести до помітної різниці за певних обставин.

Різниця на рівні байтового коду полягає в тому, що виклик нестатичного методу здійснюється наскрізь INVOKEVIRTUAL, INVOKEINTERFACEабо в INVOKESPECIALтой час як статичний виклик методу здійснюється наскрізь INVOKESTATIC.


2
Однак приватний метод екземпляра використовується (принаймні, як правило), invokespecialоскільки він не є віртуальним.
Mark Peters

Ах, цікаво, я міг думати лише про конструкторів, тому я пропустив це! Дякую! (відповідь, що оновлюється)
aioobe

2
JVM оптимізує, якщо існує один тип інстанцій. Якщо B розширює A, і жоден екземпляр B не був створений, виклики методів на A не потребуватимуть пошуку віртуальної таблиці.
Стів Куо

13

Неймовірно, що будь-яка різниця у виконанні статичних та нестатичних викликів може змінити вашу програму. Пам'ятайте, що "передчасна оптимізація - корінь усього зла".



Ви б, будь ласка, далі пояснили, що означає "" передчасна оптимізація - корінь усього зла "."
user2121

Питання було "Чи є корисність того чи іншого результату?", І це точно відповідає на це питання.
DJClayworth

13

7 років потому ...

Я не маю величезної впевненості в результатах, які знайшов Майк Накіс, оскільки вони не вирішують деяких загальних питань, пов'язаних з оптимізацією точки доступу. Я інструментував тести, використовуючи 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 тощо). Чи знаєте ви про відносно прості інструменти / способи, якими можна спробувати порівняти ці інші показники?
Райан Томас

Точка доступу виявляє, що в шляху до класу немає розширень для InstanceSum. Спробуйте додати ще один клас, який розширює InstanceSum і замінює метод.
Мілан,

12

Для рішення, якщо метод повинен бути статичним, аспект продуктивності повинен бути неактуальним. Якщо у вас проблема з продуктивністю, створення багатьох методів статичним методом не врятує день. Однак, статичні методи майже напевно не повільніші, ніж будь-який примірник, у більшості випадків незначно швидші :

1.) Статичні методи не є поліморфними, тому JVM має менше рішень для пошуку фактичного коду для виконання. Це спірний момент у епоху точки доступу, оскільки точка доступу оптимізує виклики методів екземпляра, які мають лише один сайт реалізації, тому вони будуть виконувати те саме.

2.) Ще одна тонка відмінність полягає в тому, що статичні методи, очевидно, не мають посилання "це". Це призводить до того, що кадр стека на один слот менше, ніж у методу екземпляра з однаковим підписом і тілом ("this" поміщається в слот 0 локальних змінних на рівні байт-коду, тоді як для статичних методів слот 0 використовується для першого параметр методу).


5

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

Це, безумовно, є частиною 97% малих показників ефективності, про які вам слід забути .


2
Неправильно. Ви нічого не можете припустити. Це може бути щільна петля, необхідна для інтерфейсу інтерфейсу, що може змінити велику залежність від того, наскільки "спритний" інтерфейс користувача. Наприклад, пошук TableViewмільйонів записів.
трилогія

0

Теоретично дешевше.

Статична ініціалізація буде здійснена, навіть якщо ви створите екземпляр об'єкта, тоді як статичні методи не виконують жодної ініціалізації, яка зазвичай виконується в конструкторі.

Однак я цього не перевіряв.


1
@R. Bemrose, що стосується статичної ініціалізації з цим питанням?
Кірк Волл

@Kirk Woll: Тому що статична ініціалізація робиться вперше, коли на клас посилається ..., включаючи до першого виклику статичного методу.
Powerlord

@R. Bemrose, звичайно, як і завантаження класу у VM для початку. Здається, IMO не є послідовником.
Kirk Woll,

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);
   };
};
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.