1. ホーム
  2. java

[解決済み] if (variable1 % variable2 == 0)はなぜ非効率的なのですか?

2022-04-20 14:10:31

疑問点

私はjavaの初心者で、昨晩いくつかのコードを実行していたのですが、このことがとても気になりました。私はforループでX個の出力を表示する簡単なプログラムを作っていたのですが、modulus を variable % variablevariable % 5000 とかなんとか言ってます。なぜそうなるのか、何が原因なのか、どなたか説明していただけませんか?そうすれば、私はより良くなることができます...

以下は、quot;efficient" のコードです(構文が少し間違っていたらすみません。)

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

ちなみに、私は違いを測るために日付変数を用意したのですが、それが十分長くなると、最初のものは50msで、もう一方は12秒かかるとか、そんな感じでした。を増やさないといけないかもしれません。 stopNum を減らすか progressCheck もし、あなたのPCが私より高性能であれば、あるいはそうでなければ。

この質問をウェブ上で探したのですが、答えが見つかりません、私の聞き方が悪いだけかもしれませんが。

編集部 私の質問がこんなに人気があるとは思いませんでした、すべての回答に感謝します。私はそれぞれの半分の時間でベンチマークを実行し、非効率的なコードは1/4秒対10秒とかなり長くかかりました。printlnを使用していることは確かですが、どちらも同じ量を実行しているので、特にこの不一致は再現可能なので、それが大きく影響するとは思えません。答えに関しては、私はJavaの初心者なので、どの答えがベストなのか、今のところ投票に任せることにします。水曜日までに1つ選ぶようにします。

EDIT2: modulusの代わりに変数をインクリメントし、progressCheckに到達したら1回実行し、その変数を0にリセットする第3の選択肢を作るつもりです。

EDIT3.5:

このコードを使って、以下に私の結果を示します。皆さん、素晴らしい助けをありがとうございます また、私は0に長いの短い値を比較しようとしたので、私の新しいチェックは、それが繰り返しで等しくなるように、これまで&quot;65536&quot;回数が発生します。

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

結果

  • 固定 = 874ms(通常は1000ms程度だが、2のべき乗であるため速くなる)
  • 変数 = 8590 ms
  • 最終変数 = 1944 ms (50000を使用した場合、~1000msでした)
  • インクリメント = 1904 ms
  • ショートコンバージョン=679ms

十分に驚くことではありませんが、分割ができないため、Short Conversion は "fast" 方式よりも 23% 速くなっています。これは興味深いことです。もし、256回ごと(あるいはそのくらい)に何かを表示したり比較したりする必要がある場合は、このようにし

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

最終的に興味深いのは、65536(きれいな数字ではありません)で"Final declared Variable"でmodulusを使用すると、固定値より半分の速度(遅い)だったことです。以前はほぼ同じ速度でベンチマークしていたところです。

解決方法は?

を測定しています。 OSR(オンスタック・リプレイスメント) スタブです。

OSRスタブ は、コンパイルされたメソッドの特別なバージョンで、メソッドの実行中に解釈モードからコンパイルされたコードに実行を移すことを特に意図しています。

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

ここでも似たようなことが起こります。非効率的なコード"が長いループを実行している間、メソッドはループのすぐ内側でオンスタック置換のために特別にコンパイルされます。解釈されたフレームからOSRコンパイルされたメソッドに状態が転送され、この状態には progressCheck ローカル変数 この時点では、JIT は変数を定数に置き換えることができないので、以下のような最適化を適用することができません。 強度低減 .

特に、これはJITが置き換えられないことを意味します。 整数分割 乗算 . (参照 なぜ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スタブのために、確かにかなり遅いです。しかし、このメソッドが最初から再実行されるとすぐに、利用可能なすべてのコンパイラ最適化を活用した、新しい無制約バージョンが実行されます。