web-dev-qa-db-ja.com

if(variable1%variable2 == 0)が非効率なのはなぜですか?

私は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");
    }
}

結果:

  • 固定= 874ミリ秒(通常は約1000ミリ秒ですが、2のべき乗であるため高速です)
  • 変数= 8590ミリ秒
  • 最終変数= 1944ミリ秒(50000を使用した場合は〜1000ミリ秒でした)
  • 増分= 1904ミリ秒
  • 短い変換= 679ミリ秒

十分に驚くことではありませんが、分割がないため、ショートコンバージョンは「高速」方法よりも23%高速でした。これは興味深いことです。 256回ごとに(またはそこについて)何かを表示または比較する必要がある場合は、これを行うことができます

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

65536(きれいな数字ではない)で「最終宣言変数」にモジュラスを使用した1つの最終的な興味深い注は、固定値の半分の速度(遅い)でした。以前は、同じ速度に近いベンチマークでした。

178

OSR(on-stack replacement) スタブを測定しています。

OSRスタブは、メソッドの実行中にインタープリターモードからコンパイル済みコードに実行を転送することを特に目的としたコンパイル済みメソッドの特別なバージョンです。

OSRスタブは、解釈されたフレームと互換性のあるフレームレイアウトを必要とするため、通常のメソッドほど最適化されていません。これは、次の回答で既に示しました: 12 、。

ここでも同様のことが起こります。 「非効率的なコード」が長いループを実行している間、メソッドはループ内でスタック上の置換のために特別にコンパイルされます。状態は、解釈されたフレームからOSRでコンパイルされたメソッドに転送され、この状態にはprogressCheckローカル変数が含まれます。この時点で、JITは変数を定数に置き換えることができないため、 strength reduction などの特定の最適化を適用できません。

特に、これは、JITがinteger divisionmultiplicationに置き換えないことを意味します。 ( なぜ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スタブが非効率的にコンパイルされているため、実際には非常に遅くなります。しかし、メソッドが最初から再実行されるとすぐに、利用可能なすべてのコンパイラ最適化を活用する新しい制約のないバージョンが実行されます。

139
apangin

@ 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

42

他の人が指摘したように、一般的なモジュラス演算では、除算を行う必要があります。場合によっては、除算を(コンパイラーによって)乗算に置き換えることができます。ただし、両方とも加算/減算に比べて時間がかかる場合があります。したがって、これらの線に沿って何かによって最高のパフォーマンスが期待できます。

_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を上回る場合があります。

26
JimmyB

また、上記のコードのパフォーマンスを見て驚いた。それはすべて、宣言された変数に従ってプログラムを実行するためにコンパイラーが要する時間です。 2番目の(非効率的な)例:

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

2つの変数間でモジュラス演算を実行しています。ここで、コンパイラはstopNumおよびprogressCheckの値をチェックして、各反復後に毎回これらの変数に配置されている特定のメモリブロックに移動する必要があります。

それが、各反復コンパイラーが変数の最新の値をチェックするためにメモリーの場所に行った理由です。そのため、コンパイル時にコンパイラは効率的なバイトコードを作成できませんでした。

最初のコード例では、実行中に変更されない変数と定数数値の間でモジュラス演算子を実行しており、コンパイラーはメモリー位置からその数値の値をチェックする必要がありません。コンパイラが効率的なバイトコードを作成できたのはそのためです。 progressCheckfinalまたはfinal static変数として宣言すると、ランタイム/コンパイル時コンパイラーは、それが最終変数であり、その値が進まないことを認識します。変更するには、コンパイラーがコードのprogressCheck50000に置き換えます。

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

これで、このコードは最初の(効率的な)コード例のように見えることがわかります。最初のコードのパフォーマンスと、前述のように両方のコードが効率的に機能します。どちらのコード例の実行時間にも大きな違いはありません。

4
Bishal Dubey