Czy wywołania statyczne Java są droższe czy tańsze niż wywołania statyczne?

96

Czy są jakieś korzyści związane z wydajnością? Czy jest to specyficzne dla kompilatora / maszyny wirtualnej? Używam Hotspot.

Andy Faibishenko
źródło

Odpowiedzi:

73

Po pierwsze: nie powinieneś dokonywać wyboru statycznego lub niestatycznego na podstawie wydajności.

Po drugie: w praktyce nie będzie to miało znaczenia. Hotspot może zdecydować się na optymalizację w taki sposób, aby połączenia statyczne były szybsze dla jednej metody, a połączenia niestatyczne szybciej dla innej.

Po trzecie: wiele mitów otaczających statyczne i niestatyczne jest oparte albo na bardzo starych maszynach JVM (które nie były w żaden sposób zbliżone do optymalizacji, jaką robi Hotspot) lub na kilku zapamiętanych ciekawostkach dotyczących C ++ (w których dynamiczne wywołanie używa jeszcze jednego dostępu do pamięci niż połączenie statyczne).

Zaraz
źródło
1
Masz całkowitą rację, nie powinieneś preferować metod statycznych opartych wyłącznie na tym. Jednak w przypadku, gdy metody statyczne dobrze pasują do projektu, warto wiedzieć, że są co najmniej tak samo szybkie, jeśli nie szybsze niż metody instancji i nie należy ich wykluczać ze względu na wydajność.
Will
2
@AaronDigulla -.- co jeśli powiem ci, że przyszedłem tutaj, ponieważ optymalizuję teraz, nie przedwcześnie, ale kiedy naprawdę tego potrzebuję? Zakładałeś, że OP chce zoptymalizować przedwcześnie, ale wiesz, że ta witryna jest trochę globalna ... prawda? Nie chcę być niegrzeczny, ale następnym razem nie zakładaj takich rzeczy.
Dalibor Filus
1
@DaliborFilus Muszę znaleźć równowagę. Używanie metod statycznych powoduje różnego rodzaju problemy, dlatego należy ich unikać, zwłaszcza gdy nie wiesz, co robisz. Po drugie, większość „wolnego” kodu jest spowodowana (złym) projektem, a nie tym, że język wyboru jest powolny. Jeśli twój kod jest powolny, statyczne metody prawdopodobnie go nie zapiszą, chyba że ich metody wywołujące absolutnie nic nie robią . W większości przypadków kod w metodach przyćmiewa narzut wywołania.
Aaron Digulla
7
Głosowano w dół. To nie odpowiada na pytanie. Pytanie zadawane o korzyści związane z wydajnością. Nie pytał o opinie na temat zasad projektowania.
Colm Bhandal
4
Gdybym wyszkolił papugę, by mówiła, że ​​„źródłem wszelkiego zła jest przedwczesna optymalizacja”, dostałbym 1000 głosów od ludzi, którzy wiedzą tyle samo co papuga o wydajności.
rghome
62

Cztery lata później...

Okay, mając nadzieję na rozwiązanie tego problemu raz na zawsze, napisałem test porównawczy, który pokazuje, jak różne rodzaje połączeń (wirtualne, niewirtualne, statyczne) porównują się ze sobą.

Uruchomiłem go na ideone i oto co mam:

(Większa liczba iteracji jest lepsza).

    Success time: 3.12 memory: 320576 signal:0
  Name          |  Iterations
    VirtualTest |  128009996
 NonVirtualTest |  301765679
     StaticTest |  352298601
Done.

Zgodnie z oczekiwaniami wywołania metod wirtualnych są najwolniejsze, wywołania metod niewirtualnych są szybsze, a wywołania metod statycznych są jeszcze szybsze.

Nie spodziewałem się, że różnice będą tak wyraźne: zmierzono, że wywołania metod wirtualnych działały z mniej niż połową szybkości wywołań metod niewirtualnych, które z kolei działały o całe 15% wolniej niż wywołania statyczne. To właśnie pokazują te pomiary; rzeczywiste różnice muszą być w rzeczywistości nieco bardziej wyraźne, ponieważ dla każdego wywołania metody wirtualnej, nie wirtualnej i statycznej mój kod porównawczy ma dodatkowy stały narzut zwiększania jednej zmiennej całkowitej, sprawdzania zmiennej boolowskiej i zapętlania, jeśli nie jest prawdą.

Przypuszczam, że wyniki będą się różnić w zależności od procesora i od JVM do JVM, więc spróbuj i zobacz, co otrzymasz:

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) {}
    } );
}

Warto zauważyć, że ta różnica w wydajności ma zastosowanie tylko do kodu, który nie robi nic innego niż wywoływanie metod bez parametrów. Jakikolwiek inny kod, który masz między wywołaniami, osłabi różnice, co obejmuje przekazywanie parametrów. W rzeczywistości 15% różnica między wywołaniami statycznymi i niewirtualnymi jest prawdopodobnie w pełni wyjaśniona przez fakt, że thiswskaźnik nie musi być przekazywany do metody statycznej. Tak więc wystarczyłaby tylko dość mała ilość kodu wykonującego trywialne rzeczy pomiędzy wywołaniami, aby różnica między różnymi rodzajami wywołań została rozcieńczona do punktu, w którym nie miałoby żadnego wpływu netto.

Nie bez powodu istnieją również wywołania metod wirtualnych; mają cel do spełnienia i są wdrażane przy użyciu najbardziej wydajnych środków zapewnianych przez podstawowy sprzęt. (Zestaw instrukcji procesora). Jeśli chcąc je wyeliminować, zastępując je wywołaniami nie wirtualnymi lub statycznymi, w końcu będziesz musiał dodać nawet jotę dodatkowego kodu, aby emulować ich funkcjonalność, wówczas wynikowy narzut netto jest ograniczony nie mniej, ale więcej. Całkiem możliwe, że dużo, dużo, niewyobrażalnie dużo, więcej.

Mike Nakis
źródło
7
Termin „wirtualny” to termin C ++. W Javie nie ma metod wirtualnych. Istnieją zwykłe metody, które są polimorficzne w czasie wykonywania, oraz metody statyczne lub końcowe, które nie są.
Zhenya
18
@levgen tak, dla kogoś, kto ma punkt widzenia tak wąski, jak oficjalny przegląd języka wysokiego poziomu, jest dokładnie taki, jak mówisz. Ale oczywiście koncepcje wysokiego poziomu są implementowane przy użyciu dobrze znanych mechanizmów niskiego poziomu, które zostały wynalezione na długo przed powstaniem Java, a metody wirtualne są jedną z nich. Jeśli spojrzysz tylko pod maskę, od razu zobaczysz, że tak jest: docs.oracle.com/javase/specs/jvms/se7/html/…
Mike Nakis
13
Dziękujemy za udzielenie odpowiedzi na pytanie bez zakładania przedwczesnej optymalizacji. Świetna odpowiedź.
vegemite4me
3
Tak, dokładnie to miałem na myśli. W każdym razie właśnie przeprowadziłem test na moim komputerze. Oprócz jittera, którego można się spodziewać po takim benchmarku, nie ma żadnej różnicy w szybkości: VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198na mojej instalacji OpenJDK. FTR: To prawda, nawet jeśli usunę finalmodyfikator. Przy okazji. Musiałem zrobić terminatepole volatile, inaczej test się nie skończył.
Marten
4
FYI, mam dość zaskakujące wyniki na Nexusa 5 z Androidem 6: VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. Nie dość, że OpenJDK na moim notebooku jest w stanie wykonać 40 razy więcej iteracji, test statyczny zawsze ma około 30% mniejszą przepustowość. Może to być zjawisko specyficzne dla ART, ponieważ otrzymuję oczekiwany wynik na tablecie z Androidem 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten
45

Cóż, wywołań statycznych nie można przesłonić (więc zawsze są kandydatami do wstawiania) i nie wymagają żadnych sprawdzeń nieważności. HotSpot wykonuje kilka fajnych optymalizacji, na przykład metod, które mogą równie dobrze negować te zalety, ale są one możliwe powody, dla których statyczne wywołanie może być szybsze.

Nie powinno to jednak wpływać na Twój projekt - kodowanie w najbardziej czytelny, naturalny sposób - i martw się o tego rodzaju mikro-optymalizację tylko wtedy, gdy masz tylko przyczynę (której prawie nigdy nie będziesz).

Jon Skeet
źródło
Są to możliwe powody, dla których statyczne połączenie może być szybsze. Czy możesz mi wyjaśnić te powody?
JavaTechnical
6
@JavaTechnical: Odpowiedź wyjaśnia te powody - bez nadpisywania (co oznacza, że ​​nie musisz opracowywać implementacji do użycia za każdym razem i możesz ją wbudować) i nie musisz sprawdzać, czy wywołujesz metodę w zerowe odniesienie.
Jon Skeet,
6
@JavaTechnical: Nie rozumiem. Właśnie podałem rzeczy, które nie muszą być obliczane / sprawdzane pod kątem metod statycznych, wraz z możliwością wbudowania. Brak pracy jest korzystny dla wydajności. Co zostało do zrozumienia?
Jon Skeet,
Czy zmienne statyczne są pobierane szybciej niż zmienne niestatyczne?
JavaTechnical
1
@JavaTechnical: Cóż, nie ma do wykonania sprawdzenia nieważności - ale jeśli kompilator JIT może usunąć to sprawdzenie (które będzie zależne od kontekstu), nie spodziewałbym się dużej różnicy. Znacznie ważniejsze byłyby kwestie takie jak to, czy pamięć jest w pamięci podręcznej.
Jon Skeet,
18

Jest to specyficzne dla kompilatora / maszyny wirtualnej.

  • Teoretycznie statyczne wywołanie może być nieco bardziej wydajne, ponieważ nie wymaga wyszukiwania funkcji wirtualnej, a także pozwala uniknąć narzutu związanego z ukrytym parametrem „this”.
  • W praktyce wiele kompilatorów i tak to zoptymalizuje.

Dlatego prawdopodobnie nie warto się tym przejmować, chyba że zidentyfikujesz to jako naprawdę krytyczny problem z wydajnością w swojej aplikacji. Przedwczesna optymalizacja jest źródłem wszelkiego zła itp ...

Jednak ja nie widziałem tego optymalizacja daje znaczny wzrost wydajności w następującej sytuacji:

  • Metoda wykonująca bardzo proste obliczenia matematyczne bez dostępu do pamięci
  • Metoda jest wywoływana miliony razy na sekundę w ciasnej wewnętrznej pętli
  • Aplikacja związana z procesorem, w której liczy się każda wydajność

Jeśli powyższe dotyczy Ciebie, warto przetestować.

Jest jeszcze jeden dobry (i potencjalnie nawet ważniejszy!) Powód, aby używać metody statycznej - jeśli metoda faktycznie ma semantykę statyczną (tj. Logicznie nie jest połączona z daną instancją klasy), to warto uczynić ją statyczną aby odzwierciedlić ten fakt. Doświadczeni programiści Java zauważą wtedy modyfikator static i natychmiast pomyślą: „aha! Ta metoda jest statyczna, więc nie wymaga instancji i prawdopodobnie nie manipuluje stanem konkretnej instancji”. Dzięki temu skutecznie przekazujesz statyczny charakter metody ...

mikera
źródło
13

Jest niewiarygodnie mało prawdopodobne, że jakakolwiek różnica w wydajności wywołań statycznych i niestatycznych ma wpływ na działanie aplikacji. Pamiętaj, że „przedwczesna optymalizacja jest źródłem wszelkiego zła”.

DJClayworth
źródło
To był Knuth! shreevatsa.wordpress.com/2008/05/16/…
ShreevatsaR
Czy mógłbyś dalej wyjaśnić, co oznacza „„ źródłem wszelkiego zła jest przedwczesna optymalizacja ”.
user2121
Pytanie brzmiało „Czy jest jakaś korzyść dla wydajności w ten czy inny sposób?” I to dokładnie odpowiada na to pytanie.
DJClayworth
13

Jak powiedzieli poprzednie postery: Wydaje się, że to przedwczesna optymalizacja.

Jest jednak jedna różnica (część z faktu, że wywołania niestatyczne wymagają dodatkowego wypchnięcia obiektu wywoływanego na stos operandów):

Ponieważ metod statycznych nie można zastąpić, nie będzie żadnych wirtualnych wyszukiwań w czasie wykonywania dla wywołania metody statycznej. W pewnych okolicznościach może to spowodować zauważalną różnicę.

Różnica na poziomie kodu bajtowego polega na tym, że wywołanie metody niestatycznej jest wykonywane za pośrednictwem INVOKEVIRTUAL, INVOKEINTERFACElub INVOKESPECIALpodczas gdy wywołanie metody statycznej jest wykonywane za pośrednictwem INVOKESTATIC.

aioobe
źródło
2
Jednak metoda wystąpienia prywatnego jest (przynajmniej zazwyczaj) wywoływana przy użyciu, invokespecialponieważ nie jest wirtualna.
Mark Peters
Ach, ciekawe, mogłem myśleć tylko o konstruktorach, dlatego go pominąłem! Dzięki! (aktualizacja odpowiedzi)
aioobe
2
JVM zoptymalizuje się, jeśli zostanie utworzony jeden typ. Jeśli B rozszerza A i żadne wystąpienie B nie zostało utworzone, wywołania metod na A nie będą wymagały wyszukiwania w wirtualnej tabeli.
Steve Kuo
13

7 lat później ...

Nie mam dużej pewności co do wyników, które znalazł Mike Nakis, ponieważ nie rozwiązują one niektórych typowych problemów związanych z optymalizacją Hotspot. Dokonałem instrumentacji testów porównawczych za pomocą JMH i stwierdziłem, że narzut metody instancji wynosi około 0,75% na moim komputerze w porównaniu z wywołaniem statycznym. Biorąc pod uwagę ten niski narzut, myślę, że z wyjątkiem operacji najbardziej wrażliwych na opóźnienia, prawdopodobnie nie jest to największy problem w projektowaniu aplikacji. Podsumowanie wyników mojego benchmarku JMH jest następujące;

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

Możesz spojrzeć na kod tutaj na Github;

https://github.com/nfisher/svsi

Sam test porównawczy jest dość prosty, ale ma na celu zminimalizowanie eliminacji martwego kodu i ciągłego zwijania. Prawdopodobnie istnieją inne optymalizacje, które przeoczyłem / przeoczyłem i te wyniki mogą się różnić w zależności od wersji JVM i systemu operacyjnego.

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);
    }
}
Nathan
źródło
1
Zainteresowanie czysto akademickie. Jestem ciekawy potencjalnych korzyści, jakie ten rodzaj mikrooptymalizacji może przynieść na metryki inne niż ops/sgłównie w środowisku ART (np. Zużycie pamięci, zmniejszony rozmiar pliku .oat itp.). Czy znasz jakieś stosunkowo proste narzędzia / sposoby, w jakie można by spróbować porównać te inne wskaźniki?
Ryan Thomas
Stwierdzono, że Hotspot nie ma żadnych rozszerzeń InstanceSum w ścieżce klas. Spróbuj dodać inną klasę, która rozszerza InstanceSum i zastępuje metodę.
mediolan
12

Przy podejmowaniu decyzji, czy metoda powinna być statyczna, aspekt wydajności powinien być nieistotny. Jeśli masz problem z wydajnością, statyczne ustawienie wielu metod nie uratuje sytuacji. To powiedziawszy, metody statyczne prawie na pewno nie są wolniejsze niż jakakolwiek metoda instancji, w większości przypadków nieznacznie szybsze :

1.) Metody statyczne nie są polimorficzne, więc JVM ma mniej decyzji do podjęcia, aby znaleźć właściwy kod do wykonania. Jest to kwestia sporna w Age of Hotspot, ponieważ Hotspot zoptymalizuje wywołania metod instancji, które mają tylko jedną witrynę implementacji, więc będą wykonywać to samo.

2.) Inną subtelną różnicą jest to, że metody statyczne oczywiście nie mają „tego” odniesienia. Powoduje to, że ramka stosu o jedną szczelinę jest mniejsza niż w przypadku metody instancji z tą samą sygnaturą i treścią („this” jest umieszczane w szczelinie 0 zmiennych lokalnych na poziomie kodu bajtowego, podczas gdy w przypadku metod statycznych szczelina 0 jest używana jako pierwsza parametr metody).

Durandal
źródło
5

Może istnieć różnica i może się to udać w przypadku każdego konkretnego fragmentu kodu, a nawet może się zmienić nawet przy niewielkim wydaniu maszyny JVM.

Jest to zdecydowanie część z 97% małych wydajności, o których należy zapomnieć .

Michael Borgwardt
źródło
2
Źle. Nie możesz niczego zakładać. Może to być ciasna pętla wymagana dla interfejsu użytkownika front-end, która może mieć ogromny wpływ na to, jak „zgryźliwy” jest interfejs użytkownika. Na przykład wyszukiwanie TableViewmilionów rekordów.
trylogia
0

Teoretycznie tańsze.

Inicjalizacja statyczna zostanie wykonana nawet jeśli utworzysz instancję obiektu, podczas gdy metody statyczne nie wykonają żadnej inicjalizacji normalnie wykonywanej w konstruktorze.

Jednak nie testowałem tego.

Władca
źródło
1
@R. Bemrose, co ma wspólnego inicjalizacja statyczna z tym pytaniem?
Kirk Woll,
@Kirk Woll: Ponieważ inicjalizacja statyczna jest wykonywana przy pierwszym odwołaniu do klasy ... w tym przed pierwszym wywołaniem metody statycznej.
Powerlord
@R. Bemrose, oczywiście, podobnie jak ładowanie klasy do maszyny wirtualnej na początek. Wygląda na to, że nie jest sekwencyjny, IMO.
Kirk Woll,
0

Jak zauważa Jon, metod statycznych nie można przesłonić, więc proste wywołanie metody statycznej może być - w wystarczająco naiwnym środowisku wykonawczym Java - szybsze niż wywołanie metody instancji.

Ale nawet zakładając, że jesteś w punkcie, w którym zależy ci na zepsuciu projektu, aby zaoszczędzić kilka nanosekund, pojawia się kolejne pytanie: czy potrzebujesz metody nadpisującej siebie? Jeśli zmienisz swój kod, aby przekształcić metodę instancji w metodę statyczną, aby zaoszczędzić nanosekundę tu i tam, a następnie obrócisz i zaimplementujesz własny dyspozytor, prawie na pewno będzie mniej wydajny niż ten zbudowany do środowiska wykonawczego Java.

Rozpoznać
źródło
-2

Chciałbym dodać do innych świetnych odpowiedzi tutaj, że zależy to również od twojego przepływu, na przykład:

Public class MyDao {

   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, new MyRowMapper());
   };
};

Zwróć uwagę, że tworzysz nowy obiekt MyRowMapper dla każdego wywołania.
Zamiast tego sugeruję użycie tutaj pola statycznego.

Public class MyDao {

   private static RowMapper myRowMapper = new MyRowMapper();
   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, myRowMapper);
   };
};
Yair Zaslavsky
źródło