web-dev-qa-db-fra.com

Pourquoi if (variable1% variable2 == 0) est-il inefficace?

Je suis nouveau sur Java et exécutais du code la nuit dernière, et cela m'a vraiment dérangé. Je construisais un programme simple pour afficher toutes les sorties X dans une boucle for, et j'ai remarqué une diminution MASSIVE des performances, lorsque j'ai utilisé le module comme variable % variable contre variable % 5000 ou autre chose. Quelqu'un peut-il m'expliquer pourquoi cela se produit et quelle en est la cause? Je peux donc être meilleur ...

Voici le code "efficace" (désolé si je me trompe un peu de syntaxe je ne suis pas sur l'ordinateur avec le code en ce moment)

long startNum = 0;
long stopNum = 1000000000L;

for (long i = startNum; i <= stopNum; i++){
    if (i % 50000 == 0) {
        System.out.println(i);
    }
}

Voici le "code inefficace"

long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;

for (long i = startNum; i <= stopNum; i++){
    if (i % progressCheck == 0) {
        System.out.println(i);
    }
}

Remarquez que j'avais une variable de date pour mesurer les différences, et une fois qu'elle est devenue assez longue, la première a pris 50 ms tandis que l'autre a pris 12 secondes ou quelque chose comme ça. Vous devrez peut-être augmenter le stopNum ou diminuer le progressCheck si votre PC est plus efficace que le mien ou autre.

J'ai cherché cette question sur le Web, mais je ne trouve pas de réponse, peut-être que je ne la pose pas correctement.

EDIT: Je ne m'attendais pas à ce que ma question soit si populaire, j'apprécie toutes les réponses. J'ai effectué un benchmark sur chaque moitié du temps pris, et le code inefficace a pris beaucoup plus de temps, 1/4 de seconde vs 10 secondes de donner ou de prendre. Certes, ils utilisent println, mais ils font tous les deux la même quantité, donc je n'imagine pas que cela fausserait beaucoup, d'autant plus que l'écart est répétable. En ce qui concerne les réponses, comme je suis nouveau à Java, je vais laisser les votes décider pour l'instant quelle réponse est la meilleure. J'essaierai d'en choisir un d'ici mercredi.

EDIT2: Je vais faire un autre test ce soir, où au lieu de module, il incrémente juste une variable, et quand il atteint progressCheck, il en effectuera une, puis remettra cette variable à 0. pour une 3ème option.

EDIT3.5:

J'ai utilisé ce code, et ci-dessous je vais montrer mes résultats .. Merci à TOUS pour la merveilleuse aide! J'ai également essayé de comparer la valeur courte du long à 0, donc toutes mes nouvelles vérifications se produisent toujours "65536" fois, ce qui la rend égale en répétitions.

public class Main {


    public static void main(String[] args) {

        long startNum = 0;
        long stopNum = 1000000000L;
        long progressCheck = 65536;
        final long finalProgressCheck = 50000;
        long date;

        // using a fixed value
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if (i % 65536 == 0) {
                System.out.println(i);
            }
        }
        long final1 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        //using a variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                System.out.println(i);
            }
        }
        long final2 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();

        // using a final declared variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % finalProgressCheck == 0) {
                System.out.println(i);
            }
        }
        long final3 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        // using increments to determine progressCheck
        int increment = 0;
        for (long i = startNum; i <= stopNum; i++) {
            if (increment == 65536) {
                System.out.println(i);
                increment = 0;
            }
            increment++;

        }

        //using a short conversion
        long final4 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if ((short)i == 0) {
                System.out.println(i);
            }
        }
        long final5 = System.currentTimeMillis() - date;

                System.out.println(
                "\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
    }
}

Résultats:

  • fixe = 874 ms (normalement environ 1000 ms, mais plus rapide car il s'agit d'une puissance de 2)
  • variable = 8590 ms
  • variable finale = 1944 ms (était ~ 1000 ms lors de l'utilisation de 50000)
  • incrément = 1904 ms
  • Conversion courte = 679 ms

Pas étonnant, en raison d'un manque de division, la conversion courte était 23% plus rapide que la méthode "rapide". C'est intéressant à noter. Si vous devez montrer ou comparer quelque chose toutes les 256 fois (ou à peu près là-bas), vous pouvez le faire et utiliser

if ((byte)integer == 0) {'Perform progress check code here'}

UNE NOTE FINALE INTÉRESSANTE, en utilisant le module sur la "Variable déclarée finale" avec 65536 (pas un joli nombre) était la moitié de la vitesse (plus lente) que la valeur fixe. Où avant, c'était un benchmarking proche de la même vitesse.

178
Robert Cotterman

Vous mesurez le talon OSR (remplacement sur pile) .

Le stub OSR est une version spéciale de la méthode compilée destinée spécifiquement au transfert de l'exécution du mode interprété au code compilé pendant que la méthode est en cours d'exécution.

Les stubs OSR ne sont pas aussi optimisés que les méthodes normales, car ils ont besoin d'une disposition de cadre compatible avec le cadre interprété. Je l'ai déjà montré dans les réponses suivantes: 1 , 2 , .

Une chose similaire se produit ici aussi. Alors que le "code inefficace" exécute une longue boucle, la méthode est compilée spécialement pour le remplacement sur pile directement dans la boucle. L'état est transféré de la trame interprétée vers la méthode compilée par OSR, et cet état inclut la variable locale progressCheck. À ce stade, JIT ne peut pas remplacer la variable par la constante et ne peut donc pas appliquer certaines optimisations comme réduction de la force .

En particulier, cela signifie que JIT ne remplace pas la division entière par la multiplication . (Voir Pourquoi GCC utilise-t-il la multiplication par un nombre étrange dans l'implémentation de la division entière? pour l'astuce asm d'un compilateur à l'avance, lorsque la valeur est une constante de compilation après l'inlining/constant- propagation, si ces optimisations sont activées. Un droit littéral entier dans le % l'expression est également optimisée par gcc -O0, similaire à ici où il est optimisé par le JITer même dans un stub OSR.)

Cependant, si vous exécutez plusieurs fois la même méthode, la deuxième et les exécutions suivantes exécuteront le code normal (non OSR), qui est entièrement optimisé. Voici un benchmark pour prouver la théorie ( benchmark utilisant JMH ):

@State(Scope.Benchmark)
public class Div {

    @Benchmark
    public void divConst(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % 50000 == 0) {
                blackhole.consume(i);
            }
        }
    }

    @Benchmark
    public void divVar(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;
        long progressCheck = 50000;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                blackhole.consume(i);
            }
        }
    }
}

Et les résultats:

# Benchmark: bench.Div.divConst

# Run progress: 0,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration   1: 126,967 ms/op
# Warmup Iteration   2: 105,660 ms/op
# Warmup Iteration   3: 106,205 ms/op
Iteration   1: 105,620 ms/op
Iteration   2: 105,789 ms/op
Iteration   3: 105,915 ms/op
Iteration   4: 105,629 ms/op
Iteration   5: 105,632 ms/op


# Benchmark: bench.Div.divVar

# Run progress: 50,00% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration   1: 844,708 ms/op          <-- much slower!
# Warmup Iteration   2: 105,893 ms/op          <-- as fast as divConst
# Warmup Iteration   3: 105,601 ms/op
Iteration   1: 105,570 ms/op
Iteration   2: 105,475 ms/op
Iteration   3: 105,702 ms/op
Iteration   4: 105,535 ms/op
Iteration   5: 105,766 ms/op

La toute première itération de divVar est en effet beaucoup plus lente, en raison du stub OSR compilé de manière inefficace. Mais dès que la méthode est réexécutée depuis le début, la nouvelle version non contrainte est exécutée, qui exploite toutes les optimisations de compilateur disponibles.

139
apangin

Suite à @ phuclv commentaire , j'ai vérifié le code généré par JIT1, Les résultats sont les suivants:

pour variable % 5000 (division par constante):

mov     rax,29f16b11c6d1e109h
imul    rbx
mov     r10,rbx
sar     r10,3fh
sar     rdx,0dh
sub     rdx,r10
imul    r10,rdx,0c350h    ; <-- imul
mov     r11,rbx
sub     r11,r10
test    r11,r11
jne     1d707ad14a0h

pour variable % variable:

mov     rax,r14
mov     rdx,8000000000000000h
cmp     rax,rdx
jne     22ccce218edh
xor     edx,edx
cmp     rbx,0ffffffffffffffffh
je      22ccce218f2h
cqo
idiv    rax,rbx           ; <-- idiv
test    rdx,rdx
jne     22ccce218c0h

Étant donné que la division prend toujours plus de temps que la multiplication, le dernier extrait de code est moins performant.

Version Java:

Java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

1 - VM options utilisées: -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/Java/Main.main

42
Oleksandr Pyrohov

Comme d'autres l'ont noté, le fonctionnement du module général nécessite une division. Dans certains cas, la division peut être remplacée (par le compilateur) par une multiplication. Mais les deux peuvent être lents par rapport à l'addition/soustraction. Par conséquent, les meilleures performances peuvent être attendues par quelque chose du genre:

long progressCheck = 50000;

long counter = progressCheck;

for (long i = startNum; i <= stopNum; i++){
    if (--counter == 0) {
        System.out.println(i);
        counter = progressCheck;
    }
}

(Comme tentative d'optimisation mineure, nous utilisons ici un décompteur de pré-décrémentation parce que sur de nombreuses architectures comparant à 0 Immédiatement après une opération arithmétique coûte exactement 0 instructions/cycles CPU car les drapeaux de l'ALU sont déjà définis de manière appropriée par le Un compilateur d’optimisation décent fera cependant cette optimisation automatiquement même si vous écrivez if (counter++ == 50000) { ... counter = 0; }.)

Notez que souvent vous ne voulez pas/n'avez pas vraiment besoin de module, car vous savez que votre compteur de boucles (i) ou tout ce qui n'est jamais incrémenté que de 1, et vous ne vous souciez vraiment pas du reste réel du module vous donnera, voyez simplement si le compteur incrémenté par un atteint une certaine valeur.

Une autre `` astuce '' consiste à utiliser des valeurs/limites de puissance de deux, par ex. progressCheck = 1024;. Le module une puissance de deux peut être rapidement calculé via and au niveau du bit, c'est-à-dire if ( (i & (1024-1)) == 0 ) {...}. Cela devrait être assez rapide aussi, et peut sur certaines architectures surpasser le counter explicite ci-dessus.

26
JimmyB

Je suis également surpris de voir les performances des codes ci-dessus. Il s'agit du temps mis par le compilateur pour exécuter le programme selon la variable déclarée. Dans le deuxième exemple (inefficace):

for (long i = startNum; i <= stopNum; i++) {
    if (i % progressCheck == 0) {
        System.out.println(i)
    }
}

Vous effectuez l'opération de module entre deux variables. Ici, le compilateur doit vérifier la valeur de stopNum et progressCheck pour accéder au bloc de mémoire spécifique situé pour ces variables à chaque fois après chaque itération car il s'agit d'une variable et sa valeur peut être modifiée.

C'est pourquoi, après chaque compilateur d'itérations, est allé à l'emplacement de mémoire pour vérifier la dernière valeur des variables. Par conséquent, au moment de la compilation, le compilateur n'était pas en mesure de créer un code d'octets efficace.

Dans le premier exemple de code, vous effectuez un opérateur de module entre une variable et une valeur numérique constante qui ne changera pas pendant l'exécution et le compilateur n'a pas besoin de vérifier la valeur de cette valeur numérique à partir de l'emplacement mémoire. C'est pourquoi le compilateur a pu créer un code d'octet efficace. Si vous déclarez progressCheck comme final ou comme final static variable alors au moment de l'exécution/du compilateur au moment de la compilation, sachez que c'est une variable finale et que sa valeur ne changera pas alors le compilateur remplace le progressCheck par 50000 dans du code:

for (long i = startNum; i <= stopNum; i++) {
    if (i % 50000== 0) {
        System.out.println(i)
    }
}

Vous pouvez maintenant voir que ce code ressemble également au premier exemple de code (efficace). Les performances du premier code et, comme nous l'avons mentionné ci-dessus, les deux codes fonctionneront efficacement. Il n'y aura pas beaucoup de différence dans le temps d'exécution des deux exemples de code.

4
Bishal Dubey