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:
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.
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.
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
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.
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.