私はJavaを初めて使い、昨夜コードを実行していましたが、これは本当に気になりました。 forループですべてのX出力を表示する単純なプログラムを作成していましたが、variable % variable
vs variable % 5000
またはその他としてモジュラスを使用すると、パフォーマンスが大幅に低下することに気付きました。誰かがこれがなぜで、何が原因であるかを説明できますか?だから私は良くなることができます...
ここに「効率的な」コードがあります(少し間違った構文を取得した場合は申し訳ありませんが、今はコードをコンピューターにインストールしていません)
long startNum = 0;
long stopNum = 1000000000L;
for (long i = startNum; i <= stopNum; i++){
if (i % 50000 == 0) {
System.out.println(i);
}
}
これが「非効率的なコード」です
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;
for (long i = startNum; i <= stopNum; i++){
if (i % progressCheck == 0) {
System.out.println(i);
}
}
違いを測定するための日付変数があり、十分に長くなると、最初の変数は50ミリ秒かかり、もう1つの変数は12秒程度かかりました。あなたのPCが私のものより効率的である場合、またはそうでない場合は、stopNum
を増やすか、progressCheck
を減らす必要があります。
ウェブ上でこの質問を探しましたが、答えが見つかりません。たぶん正しい質問をしていないだけかもしれません。
編集:私の質問がそれほど人気があるとは思っていませんでした。すべての答えに感謝します。かかった時間の半分ごとにベンチマークを実行しましたが、非効率なコードはかなり長くかかりました。1/ 4秒対10秒のギブまたはテイク。彼らがprintlnを使用していることは確かですが、どちらも同じ量をしているので、特に矛盾が繰り返されるので、それがそれを大きく歪めるとは思いません。答えについては、私はJavaに慣れていないので、今のところ、どちらの答えが最適かを投票で決定します。私は水曜日までに一つを選んでみます。
EDIT2:今晩、別のテストを行います。ここでは、モジュラスの代わりに変数をインクリメントし、progressCheckに達すると、1つを実行し、3番目のオプションでその変数を0にリセットします。
EDIT3.5:
私はこのコードを使用しました。以下に結果を示します。すばらしい助けをありがとう。また、longのshort値を0と比較してみたので、新しいチェックはすべて「65536」回行われ、繰り返しが等しくなります。
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");
}
}
結果:
十分に驚くことではありませんが、分割がないため、ショートコンバージョンは「高速」方法よりも23%高速でした。これは興味深いことです。 256回ごとに(またはそこについて)何かを表示または比較する必要がある場合は、これを行うことができます
if ((byte)integer == 0) {'Perform progress check code here'}
65536(きれいな数字ではない)で「最終宣言変数」にモジュラスを使用した1つの最終的な興味深い注は、固定値の半分の速度(遅い)でした。以前は、同じ速度に近いベンチマークでした。
OSR(on-stack replacement) スタブを測定しています。
OSRスタブは、メソッドの実行中にインタープリターモードからコンパイル済みコードに実行を転送することを特に目的としたコンパイル済みメソッドの特別なバージョンです。
OSRスタブは、解釈されたフレームと互換性のあるフレームレイアウトを必要とするため、通常のメソッドほど最適化されていません。これは、次の回答で既に示しました: 1 、 2 、。
ここでも同様のことが起こります。 「非効率的なコード」が長いループを実行している間、メソッドはループ内でスタック上の置換のために特別にコンパイルされます。状態は、解釈されたフレームからOSRでコンパイルされたメソッドに転送され、この状態にはprogressCheck
ローカル変数が含まれます。この時点で、JITは変数を定数に置き換えることができないため、 strength reduction などの特定の最適化を適用できません。
特に、これは、JITがinteger divisionをmultiplicationに置き換えないことを意味します。 ( なぜGCCは整数除算の実装で奇妙な数値による乗算を使用するのか? インライン/定数の後の値がコンパイル時の定数である場合、事前コンパイラからのasmトリックについてそれらの最適化が有効な場合、伝播。%
式の整数リテラルもgcc -O0
によって最適化されます。これは、OSRスタブでもJITerによって最適化される場合と同様です。)
ただし、同じメソッドを複数回実行すると、2回目以降の実行で、完全に最適化された通常の(OSR以外の)コードが実行されます。理論を証明するベンチマークを次に示します( 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);
}
}
}
}
そして結果:
# 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
divVar
の最初の反復は、OSRスタブが非効率的にコンパイルされているため、実際には非常に遅くなります。しかし、メソッドが最初から再実行されるとすぐに、利用可能なすべてのコンパイラ最適化を活用する新しい制約のないバージョンが実行されます。
@ phuclvcomment へのフォローアップで、JITによって生成されたコードをチェックしました1、結果は次のとおりです。
にとって variable % 5000
(定数による除算):
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
にとって 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
除算は常に乗算より時間がかかるため、最後のコードスニペットのパフォーマンスは低下します。
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使用されるオプション:-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/Java/Main.main
他の人が指摘したように、一般的なモジュラス演算では、除算を行う必要があります。場合によっては、除算を(コンパイラーによって)乗算に置き換えることができます。ただし、両方とも加算/減算に比べて時間がかかる場合があります。したがって、これらの線に沿って何かによって最高のパフォーマンスが期待できます。
_long progressCheck = 50000;
long counter = progressCheck;
for (long i = startNum; i <= stopNum; i++){
if (--counter == 0) {
System.out.println(i);
counter = progressCheck;
}
}
_
(ALUのフラグはすでに適切に設定されているため、算術演算の直後に_0
_と比較する多くのアーキテクチャで正確に0命令/ CPUサイクルかかるため、ここでは事前最適化のダウンカウンターを使用します。操作。ただし、まともな最適化コンパイラは、if (counter++ == 50000) { ... counter = 0; }
と書いても自動的に最適化を行います。)
ループカウンター(i
)または1だけインクリメントされるものを知っているため、実際にモジュラスを必要としない/必要としないことが多いことに注意してください。 1ずつ増加するカウンタが何らかの値にヒットするかどうかを確認します。
もう1つの「トリック」は、2のべき乗の値/制限を使用することです。 _progressCheck = 1024;
_。モジュラス2のべき乗は、ビット単位のand
、つまりif ( (i & (1024-1)) == 0 ) {...}
を介してすばやく計算できます。これも非常に高速で、一部のアーキテクチャでは上記の明示的なcounter
を上回る場合があります。
また、上記のコードのパフォーマンスを見て驚いた。それはすべて、宣言された変数に従ってプログラムを実行するためにコンパイラーが要する時間です。 2番目の(非効率的な)例:
for (long i = startNum; i <= stopNum; i++) {
if (i % progressCheck == 0) {
System.out.println(i)
}
}
2つの変数間でモジュラス演算を実行しています。ここで、コンパイラはstopNum
およびprogressCheck
の値をチェックして、各反復後に毎回これらの変数に配置されている特定のメモリブロックに移動する必要があります。
それが、各反復コンパイラーが変数の最新の値をチェックするためにメモリーの場所に行った理由です。そのため、コンパイル時にコンパイラは効率的なバイトコードを作成できませんでした。
最初のコード例では、実行中に変更されない変数と定数数値の間でモジュラス演算子を実行しており、コンパイラーはメモリー位置からその数値の値をチェックする必要がありません。コンパイラが効率的なバイトコードを作成できたのはそのためです。 progressCheck
をfinal
またはfinal static
変数として宣言すると、ランタイム/コンパイル時コンパイラーは、それが最終変数であり、その値が進まないことを認識します。変更するには、コンパイラーがコードのprogressCheck
を50000
に置き換えます。
for (long i = startNum; i <= stopNum; i++) {
if (i % 50000== 0) {
System.out.println(i)
}
}
これで、このコードは最初の(効率的な)コード例のように見えることがわかります。最初のコードのパフォーマンスと、前述のように両方のコードが効率的に機能します。どちらのコード例の実行時間にも大きな違いはありません。