Les appels statiques Java sont-ils plus ou moins chers que les appels non statiques?

95

Y a-t-il un avantage de performance d'une manière ou d'une autre? Est-ce spécifique au compilateur / VM? J'utilise Hotspot.

Andy Faibishenko
la source

Réponses:

74

Premièrement: vous ne devriez pas faire le choix entre statique et non statique en fonction des performances.

Deuxièmement: en pratique, cela ne fera aucune différence. Hotspot peut choisir d'optimiser de manière à accélérer les appels statiques pour une méthode et les appels non statiques plus rapidement pour une autre.

Troisièmement: une grande partie du mythe entourant le statique par rapport au non-statique est basée soit sur de très anciennes machines virtuelles Java (qui ne se rapprochent pas de l'optimisation que fait Hotspot), soit sur des anecdotes mémorisées sur C ++ (dans lequel un appel dynamique utilise un autre accès mémoire qu'un appel statique).

Anon
la source
1
Vous avez tout à fait raison, vous ne devriez pas préférer les méthodes statiques basées uniquement sur cela. Cependant, dans le cas où les méthodes statiques correspondent bien à la conception, il est utile de savoir qu'elles sont au moins aussi rapides, sinon plus rapides, que les méthodes d'instance et ne doivent pas être exclues sur la base des performances.
Sera
2
@AaronDigulla -.- Et si je vous disais que je suis venu ici parce que j'optimise en ce moment, pas prématurément, mais quand j'en ai vraiment besoin? Vous avez supposé qu'OP voulait optimiser prématurément, mais vous savez que ce site est un peu global ... Non? Je ne veux pas être impoli, mais s'il vous plaît, ne présumez pas des choses comme ça la prochaine fois.
Dalibor Filus
1
@DaliborFilus J'ai besoin de trouver un équilibre. L'utilisation de méthodes statiques pose toutes sortes de problèmes, il faut donc les éviter, surtout lorsque vous ne savez pas ce que vous faites. Deuxièmement, la plupart du code «lent» est dû à une (mauvaise) conception, et non à la lenteur de la langue de choix. Si votre code est lent, les méthodes statiques ne le sauveront probablement pas à moins que ses méthodes d'appel ne font absolument rien . Dans la plupart des cas, le code dans les méthodes éclipsera les frais généraux d'appel.
Aaron Digulla
6
Voté contre. Cela ne répond pas à la question. La question posée sur les avantages de performance. Il n'a pas demandé d'avis sur les principes de conception.
Colm Bhandal
4
Si j'entraînais un perroquet à dire que "l'opimisation prématurée est la racine de tout mal", j'obtiendrais 1000 voix de personnes qui en savent autant sur la performance que le perroquet.
rghome
62

Quatre ans plus tard...

D'accord, dans l'espoir de régler cette question une fois pour toutes, j'ai écrit un benchmark qui montre comment les différents types d'appels (virtuels, non virtuels, statiques) se comparent les uns aux autres.

Je l'ai couru sur ideone , et voici ce que j'ai obtenu:

(Un plus grand nombre d'itérations est préférable.)

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

Comme prévu, les appels de méthodes virtuelles sont les plus lents, les appels de méthodes non virtuelles sont plus rapides et les appels de méthodes statiques sont encore plus rapides.

Ce à quoi je ne m'attendais pas, c'était que les différences soient si prononcées: les appels de méthodes virtuelles ont été mesurés pour s'exécuter à moins de la moitié de la vitesse des appels de méthodes non virtuelles, qui à leur tour ont été mesurés pour s'exécuter 15% plus lentement. que les appels statiques. C'est ce que montrent ces mesures; les différences réelles doivent en fait être légèrement plus prononcées, car pour chaque appel de méthode virtuelle, non virtuelle et statique, mon code d'analyse comparative a une surcharge constante supplémentaire consistant à incrémenter une variable entière, à vérifier une variable booléenne et à boucler si ce n'est pas vrai.

Je suppose que les résultats varieront d'un processeur à l'autre et de JVM à JVM, alors essayez-le et voyez ce que vous obtenez:

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

Il convient de noter que cette différence de performances ne s'applique qu'au code qui ne fait rien d'autre que l'appel de méthodes sans paramètre. Tout autre code que vous avez entre les appels diluera les différences, et cela inclut le passage de paramètres. En fait, la différence de 15% entre les appels statiques et non virtuels s'explique probablement entièrement par le fait que lethis pointeur n'a pas à être passé à la méthode statique. Donc, il ne faudrait qu'une assez petite quantité de code pour faire des choses triviales entre les appels pour que la différence entre les différents types d'appels soit diluée au point de n'avoir aucun impact net.

En outre, les appels de méthode virtuelle existent pour une raison; ils ont un but à servir, et ils sont mis en œuvre en utilisant les moyens les plus efficaces fournis par le matériel sous-jacent. (Le jeu d'instructions du processeur.) Si, dans votre désir de les éliminer en les remplaçant par des appels non virtuels ou statiques, vous finissez par devoir ajouter jusqu'à un iota de code supplémentaire pour émuler leur fonctionnalité, alors la surcharge nette qui en résulte est liée ne pas être moins, mais plus. Très probablement, beaucoup, beaucoup, insondable beaucoup, plus.

Mike Nakis
la source
7
«Virtuel» est un terme C ++. Il n'y a pas de méthodes virtuelles en Java. Il existe des méthodes ordinaires, qui sont polymorphes à l'exécution, et des méthodes statiques ou finales, qui ne le sont pas.
Zhenya
16
@levgen oui, pour quelqu'un dont le point de vue est aussi étroit que l'aperçu officiel de haut niveau de la langue, c'est précisément ce que vous dites. Mais bien sûr, les concepts de haut niveau sont mis en œuvre à l'aide de mécanismes de bas niveau bien établis qui ont été inventés bien avant que java n'ait vu le jour, et les méthodes virtuelles en font partie. Si vous jetez juste un petit coup d'œil sous le capot, vous verrez immédiatement qu'il en est ainsi: docs.oracle.com/javase/specs/jvms/se7/html/…
Mike Nakis
13
Merci d'avoir répondu à la question sans faire de présomptions sur l'optimisation prématurée. Très bonne réponse.
vegemite4me
3
Oui, c'est exactement ce que je voulais dire. Quoi qu'il en soit, je viens de faire le test sur ma machine. Hormis le jitter auquel vous pouvez vous attendre pour un tel benchmark, il n'y a aucune différence de vitesse: VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198sur mon installation OpenJDK. FTR: C'est même vrai si je supprime le finalmodificateur. Btw. J'ai dû faire le terminateterrain volatile, sinon le test ne s'est pas terminé.
Marten
4
Pour votre information, je reçois des résultats assez surprenants sur un Nexus 5 fonctionnant sous Android 6: VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170. Non seulement OpenJDK sur mon ordinateur portable parvient à effectuer 40 fois plus d'itérations, mais le test statique a toujours un débit inférieur d'environ 30%. Cela pourrait être un phénomène spécifique à l'ART, car j'obtiens un résultat attendu sur une tablette Android 4.4:VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten
46

Eh bien, les appels statiques ne peuvent pas être remplacés (ils sont donc toujours candidats à l'inlining) et ne nécessitent aucune vérification de nullité. HotSpot fait un tas d'optimisations intéressantes, par exemple des méthodes qui peuvent bien annuler ces avantages, mais ce sont des raisons possibles pour lesquelles un appel statique peut être plus rapide.

Cependant, cela ne devrait pas affecter votre conception - code de la manière la plus lisible et la plus naturelle - et ne vous soucier de ce type de micro-optimisation que si vous avez une juste cause (ce que vous ne ferez presque jamais ).

Jon Skeet
la source
Ce sont des raisons possibles pour lesquelles un appel statique peut être plus rapide. Pouvez-vous m'expliquer ces raisons?
JavaTechnical
6
@JavaTechnical: La réponse explique ces raisons - pas de remplacement (ce qui signifie que vous n'avez pas besoin de travailler sur l'implémentation à utiliser à chaque fois et que vous pouvez en ligne) et vous n'avez pas besoin de vérifier si vous appelez la méthode sur un référence nulle.
Jon Skeet
6
@JavaTechnical: Je ne comprends pas. Je viens de vous donner des choses qui n'ont pas besoin d'être calculées / vérifiées pour les méthodes statiques, ainsi qu'une opportunité en ligne. Ne pas faire de travail est un avantage de performance. Que reste-t-il à comprendre?
Jon Skeet
Les variables statiques sont-elles récupérées plus rapidement que les variables non statiques?
JavaTechnical
1
@JavaTechnical: Eh bien, il n'y a pas de vérification de nullité à effectuer - mais si le compilateur JIT peut supprimer cette vérification (qui sera spécifique au contexte), je ne m'attendrais pas à beaucoup de différence. Des choses comme si la mémoire est dans le cache seraient beaucoup plus importantes.
Jon Skeet
18

Il est spécifique au compilateur / VM.

  • En théorie , un appel statique peut être rendu légèrement plus efficace car il n'a pas besoin de faire une recherche de fonction virtuelle, et il peut également éviter la surcharge du paramètre caché "this".
  • En pratique , de nombreux compilateurs optimiseront cela de toute façon.

Par conséquent, cela ne vaut probablement pas la peine de se préoccuper à moins que vous n'ayez identifié cela comme un problème de performances vraiment critique dans votre application. L'optimisation prématurée est la racine de tout mal etc ...

Cependant , je l' ai vu cette optimisation donnent une augmentation substantielle des performances dans la situation suivante:

  • Méthode effectuant un calcul mathématique très simple sans accès mémoire
  • Méthode appelée des millions de fois par seconde dans une boucle interne étroite
  • Application liée au processeur où chaque bit de performance compte

Si ce qui précède s'applique à vous, cela peut valoir la peine d'être testé.

Il y a aussi une autre bonne raison (et potentiellement encore plus importante!) D'utiliser une méthode statique - si la méthode a réellement une sémantique statique (c'est-à-dire n'est pas connectée logiquement à une instance donnée de la classe) alors il est logique de la rendre statique pour refléter ce fait. Les programmeurs Java expérimentés remarqueront alors le modificateur static et penseront immédiatement "aha! Cette méthode est statique donc elle n'a pas besoin d'une instance et ne manipule probablement pas l'état spécifique de l'instance". Vous aurez donc communiqué efficacement le caractère statique de la méthode ...

Mikera
la source
14

Comme l'ont dit les affiches précédentes: cela semble être une optimisation prématurée.

Cependant, il y a une différence (une partie du fait que les invocations non statiques nécessitent une poussée supplémentaire d'un objet appelé sur la pile d'opérandes):

Étant donné que les méthodes statiques ne peuvent pas être remplacées, il n'y aura aucune recherche virtuelle au moment de l'exécution pour un appel de méthode statique. Cela peut entraîner une différence observable dans certaines circonstances.

La différence sur un code d'octet est qu'un appel de méthode non statique se fait à travers INVOKEVIRTUAL, INVOKEINTERFACEou INVOKESPECIALpendant un appel de méthode statique se fait à travers INVOKESTATIC.

aioobe
la source
2
Une méthode d'instance privée, cependant, est (au moins généralement) appelée en utilisant invokespecialcar elle n'est pas virtuelle.
Mark Peters du
Ah, intéressant, je ne pouvais penser qu'aux constructeurs, c'est pourquoi je l'ai omis! Merci! (mise à jour de la réponse)
aioobe
2
La JVM optimisera si un type est instancié. Si B étend A et qu'aucune instance de B n'a été instanciée, les appels de méthode sur A n'auront pas besoin d'une recherche de table virtuelle.
Steve Kuo du
13

Il est incroyablement improbable qu'une différence de performance entre les appels statiques et non statiques fasse une différence dans votre application. N'oubliez pas que "l'optimisation prématurée est la racine de tout mal".

DJClayworth
la source
C'était Knuth! shreevatsa.wordpress.com/2008/05/16/…
ShreevatsaR
pourriez-vous s'il vous plaît expliquer davantage ce que "" l'optimisation prématurée est la racine de tout mal "."?
user2121
La question était "Y a-t-il des avantages de performance d'une manière ou d'une autre?", Et cela répond exactement à cette question.
DJClayworth
13

7 ans plus tard ...

Je n'ai pas une grande confiance dans les résultats obtenus par Mike Nakis car ils ne résolvent pas certains problèmes courants liés aux optimisations Hotspot. J'ai instrumenté des benchmarks à l'aide de JMH et j'ai trouvé que la surcharge d'une méthode d'instance était d'environ 0,75% sur ma machine par rapport à un appel statique. Compte tenu de cette faible surcharge, je pense que, sauf dans les opérations les plus sensibles à la latence, ce n'est sans doute pas la plus grande préoccupation dans la conception d'une application. Le résumé des résultats de mon benchmark JMH est le suivant;

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

Vous pouvez consulter le code ici sur Github;

https://github.com/nfisher/svsi

Le benchmark lui-même est assez simple mais vise à minimiser l'élimination du code mort et le pliage constant. Il y a peut-être d'autres optimisations que j'ai manquées / négligées et ces résultats sont susceptibles de varier selon la version JVM et le système d'exploitation.

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
la source
1
Intérêt purement académique ici. Je suis curieux de connaître les avantages potentiels que ce type de micro-optimisation pourrait avoir sur des métriques autres que ops/sprincipalement dans un environnement ART (par exemple, utilisation de la mémoire, taille de fichier .oat réduite, etc.). Connaissez-vous des outils / moyens relativement simples dont on pourrait essayer de comparer ces autres paramètres?
Ryan Thomas
Hotspot constate qu'il n'y a pas d'extensions à InstanceSum dans le chemin de classe. Essayez d'ajouter une autre classe qui étend InstanceSum et remplace la méthode.
Milan le
12

Pour décider si une méthode doit être statique, l'aspect performance ne doit pas être pertinent. Si vous avez un problème de performances, rendre statiques de nombreuses méthodes ne sauvera pas la mise. Cela dit, les méthodes statiques ne sont presque certainement pas plus lentes que toute méthode d'instance, dans la plupart des cas légèrement plus rapide :

1.) Les méthodes statiques ne sont pas polymorphes, donc la JVM a moins de décisions à prendre pour trouver le code à exécuter. Il s'agit d'un point discutable à l'ère de Hotspot, car Hotspot optimisera les appels de méthode d'instance qui n'ont qu'un seul site d'implémentation, de sorte qu'ils effectueront la même chose.

2.) Une autre différence subtile est que les méthodes statiques n'ont évidemment pas de référence à «cette». Il en résulte une trame de pile d'un slot plus petite que celle d'une méthode d'instance avec la même signature et le même corps ("this" est placé dans le slot 0 des variables locales au niveau du bytecode, tandis que pour les méthodes statiques, le slot 0 est utilisé pour le premier paramètre de la méthode).

Durandal
la source
5

Il peut y avoir une différence, et cela peut aller dans les deux sens pour un morceau de code particulier, et cela peut changer même avec une version mineure de la JVM.

Cela fait certainement partie des 97% de petites économies que vous devriez oublier .

Michael Borgwardt
la source
2
Faux. Vous ne pouvez rien supposer. Cela pourrait être une boucle serrée requise pour une interface utilisateur frontale, ce qui pourrait faire une énorme différence sur la façon dont l'interface utilisateur est «vive». Par exemple, une recherche dans TableViewdes millions d'enregistrements.
trilogie
0

En théorie, moins cher.

L'initialisation statique va être effectuée même si vous créez une instance de l'objet, tandis que les méthodes statiques ne feront aucune initialisation normalement effectuée dans un constructeur.

Cependant, je n'ai pas testé cela.

Powerlord
la source
1
@R. Bemrose, qu'est-ce que l'initialisation statique a à voir avec cette question?
Kirk Woll du
@Kirk Woll: Parce que l'initialisation statique est effectuée la première fois que la classe est référencée ... y compris avant le premier appel de méthode statique.
Powerlord
@R. Bemrose, bien sûr, tout comme le chargement de la classe dans la machine virtuelle pour commencer. On dirait un non-séquiteur, l'OMI.
Kirk Woll le
0

Comme Jon le note, les méthodes statiques ne peuvent pas être remplacées, donc simplement invoquer une méthode statique peut être - sur un runtime Java suffisamment naïf - plus rapide que l' appel d' une méthode d'instance.

Mais alors, même en supposant que vous êtes au point où vous vous souciez de gâcher votre conception pour gagner quelques nanosecondes, cela soulève juste une autre question: aurez-vous besoin d'une méthode qui vous remplace? Si vous changez votre code pour transformer une méthode d'instance en une méthode statique pour économiser une nanoseconde ici et là, puis que vous vous retournez et implémentez votre propre répartiteur en plus de cela, le vôtre sera presque certainement moins efficace que celui construit dans votre environnement d'exécution Java déjà.

Ken
la source
-2

Je voudrais ajouter aux autres excellentes réponses ici que cela dépend également de votre flux, par exemple:

Public class MyDao {

   private String sql = "select * from MY_ITEM";

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

Faites attention que vous créez un nouvel objet MyRowMapper pour chaque appel.
Au lieu de cela, je suggère d'utiliser ici un champ statique.

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
la source