web-dev-qa-db-ja.com

なぜJavaオブジェクト参照を返すのは、プリミティブを返すよりもずっと遅いのですか?

レイテンシーに敏感なアプリケーションに取り組んでおり、あらゆる種類のメソッドをマイクロベンチマークしています( jmh を使用)。ルックアップメソッドをマイクロベンチマークし、結果に満足した後、最終バージョンを実装しましたが、最終バージョンは、ベンチマークしたばかりの3倍遅くなりました

原因は、実装されたメソッドがenumではなくintオブジェクトを返していたことです。ベンチマークコードの簡略版は次のとおりです。

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class ReturnEnumObjectVersusPrimitiveBenchmark {

    enum Category {
        CATEGORY1,
        CATEGORY2,
    }

    @Param( {"3", "2", "1" })
    String value;

    int param;

    @Setup
    public void setUp() {
        param = Integer.parseInt(value);
    }

    @Benchmark
    public int benchmarkReturnOrdinal() {
        if (param < 2) {
            return Category.CATEGORY1.ordinal();
        }
        return Category.CATEGORY2.ordinal();        
    }


    @Benchmark
    public Category benchmarkReturnReference() {
        if (param < 2) {
            return Category.CATEGORY1;
        }
        return Category.CATEGORY2;      
    }


    public static void main(String[] args) throws RunnerException {
            Options opt = new OptionsBuilder().include(ReturnEnumObjectVersusPrimitiveBenchmark.class.getName()).warmupIterations(5)
                .measurementIterations(4).forks(1).build();
        new Runner(opt).run();
    }

}

上記のベンチマーク結果:

# VM invoker: C:\Program Files\Java\jdk1.7.0_40\jre\bin\Java.exe
# VM options: -Dfile.encoding=UTF-8

Benchmark                   (value)   Mode  Samples     Score     Error   Units
benchmarkReturnOrdinal            3  thrpt        4  1059.898 ±  71.749  ops/us
benchmarkReturnOrdinal            2  thrpt        4  1051.122 ±  61.238  ops/us
benchmarkReturnOrdinal            1  thrpt        4  1064.067 ±  90.057  ops/us
benchmarkReturnReference          3  thrpt        4   353.197 ±  25.946  ops/us
benchmarkReturnReference          2  thrpt        4   350.902 ±  19.487  ops/us
benchmarkReturnReference          1  thrpt        4   339.578 ± 144.093  ops/us

関数の戻り値の型を変更するだけで、パフォーマンスがほぼ3倍に変更されました。

列挙オブジェクトと整数を返すことの唯一の違いは、一方が64ビット値(参照)を返し、もう一方が32ビット値を返すことだと思いました。私の同僚の1人は、潜在的なGCの参照を追跡する必要があるため、enumを返すとオーバーヘッドが増えると推測していました。 (ただし、enumオブジェクトは静的な最終参照であるため、それを行う必要があるのは奇妙に思えます)。

パフォーマンスの違いの説明は何ですか?


[〜#〜] update [〜#〜]

誰でもプロジェクトを複製してベンチマークを実行できるように、mavenプロジェクト here を共有しました。誰かが時間/関心を持っている場合、他の人が同じ結果を再現できるかどうかを確認することが役立つでしょう。 (Windows 64とLinux 64の2つの異なるマシンで複製しました。どちらもOracleのフレーバーJava 1.7 JVM)を使用しています。@ ZhekaKozlovは、メソッドの間に違いはないと言いました。

実行するには:(リポジトリーのクローン作成後)

mvn clean install
Java -jar .\target\microbenchmarks.jar function.ReturnEnumObjectVersusPrimitiveBenchmark -i 5 -wi 5 -f 1
73
Sam Goldberg

TL; DR:BLINDの信頼を何にも入れてはいけません。

まず最初に:実験データを確認してから結論にジャンプすることが重要です。数値を信頼するだけでなく、パフォーマンスの違いの理由を実際に追跡する必要があるため、何かが3倍速い/遅いと主張するのは奇妙です。これは、あなたのようなナノベンチマークにとって特に重要です。

第二に、実験者は自分がコントロールするものとしないものを明確に理解する必要があります。特定の例では、@Benchmarkメソッドから値を返していますが、外部の呼び出し元がプリミティブと参照に対して同じことを行うことを合理的に確信できますか?この質問を自問すると、基本的にテストインフラストラクチャを測定していることに気付くでしょう。

ポイントまで。私のマシン(i5-4210U、Linux x86_64、JDK 8u40)では、テストの結果は次のとおりです。

Benchmark                    (value)   Mode  Samples  Score   Error   Units
...benchmarkReturnOrdinal          3  thrpt        5  0.876 ± 0.023  ops/ns
...benchmarkReturnOrdinal          2  thrpt        5  0.876 ± 0.009  ops/ns
...benchmarkReturnOrdinal          1  thrpt        5  0.832 ± 0.048  ops/ns
...benchmarkReturnReference        3  thrpt        5  0.292 ± 0.006  ops/ns
...benchmarkReturnReference        2  thrpt        5  0.286 ± 0.024  ops/ns
...benchmarkReturnReference        1  thrpt        5  0.293 ± 0.008  ops/ns

さて、参照テストは3倍遅く表示されます。ただし、古いJMH(1.1.1)を使用しているため、現在の最新(1.7.1)に更新しましょう。

Benchmark                    (value)   Mode  Cnt  Score   Error   Units
...benchmarkReturnOrdinal          3  thrpt    5  0.326 ± 0.010  ops/ns
...benchmarkReturnOrdinal          2  thrpt    5  0.329 ± 0.004  ops/ns
...benchmarkReturnOrdinal          1  thrpt    5  0.329 ± 0.004  ops/ns
...benchmarkReturnReference        3  thrpt    5  0.288 ± 0.005  ops/ns
...benchmarkReturnReference        2  thrpt    5  0.288 ± 0.005  ops/ns
...benchmarkReturnReference        1  thrpt    5  0.288 ± 0.002  ops/ns

おっと、今ではかろうじて遅くなっています。ところで、これは、テストがインフラストラクチャに依存していることも示しています。さて、実際に何が起こるかを見ることができますか?

ベンチマークを構築し、@Benchmarkメソッドを正確に呼び出すものを見てみると、次のようなものが表示されます。

public void benchmarkReturnOrdinal_thrpt_jmhStub(InfraControl control, RawResults result, ReturnEnumObjectVersusPrimitiveBenchmark_jmh l_returnenumobjectversusprimitivebenchmark0_0, Blackhole_jmh l_blackhole1_1) throws Throwable {
    long operations = 0;
    long realTime = 0;
    result.startTime = System.nanoTime();
    do {
        l_blackhole1_1.consume(l_longname.benchmarkReturnOrdinal());
        operations++;
    } while(!control.isDone);
    result.stopTime = System.nanoTime();
    result.realTime = realTime;
    result.measuredOps = operations;
}

l_blackhole1_1には、値を「消費」するconsumeメソッドがあります(根拠についてはBlackholeを参照)。 Blackhole.consumeには referencesprimitives のオーバーロードがあり、それだけでパフォーマンスの違いを正当化できます。

これらのメソッドが異なるように見える理由があります。それらは、引数のタイプを可能な限り高速にしようとしています。それらを一致させようとしても、必ずしも同じパフォーマンス特性を示すとは限らないため、新しいJMHではより対称的な結果が得られます。これで、-prof perfasmにアクセスして、テスト用に生成されたコードを確認し、パフォーマンスが異なる理由を確認することもできますが、それはここではポイントです。

本当にwantが、プリミティブや参照を返すことのパフォーマンスの違いを理解するために、big scary grayを入力する必要がある場合zone微妙なパフォーマンスのベンチマーク。例えば。このテストのようなもの:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(5)
public class PrimVsRef {

    @Benchmark
    public void prim() {
        doPrim();
    }

    @Benchmark
    public void ref() {
        doRef();
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    private int doPrim() {
        return 42;
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    private Object doRef() {
        return this;
    }

}

...プリミティブと参照に対して同じ結果が得られます:

Benchmark       Mode  Cnt  Score   Error  Units
PrimVsRef.prim  avgt   25  2.637 ± 0.017  ns/op
PrimVsRef.ref   avgt   25  2.634 ± 0.005  ns/op

上で言ったように、これらのテストはrequire結果の理由をフォローアップします。この場合、両方の生成コードはほぼ同じであり、結果を説明しています。

prim:

                  [Verified Entry Point]
 12.69%    1.81%    0x00007f5724aec100: mov    %eax,-0x14000(%rsp)
  0.90%    0.74%    0x00007f5724aec107: Push   %rbp
  0.01%    0.01%    0x00007f5724aec108: sub    $0x30,%rsp         
 12.23%   16.00%    0x00007f5724aec10c: mov    $0x2a,%eax   ; load "42"
  0.95%    0.97%    0x00007f5724aec111: add    $0x30,%rsp
           0.02%    0x00007f5724aec115: pop    %rbp
 37.94%   54.70%    0x00007f5724aec116: test   %eax,0x10d1aee4(%rip)        
  0.04%    0.02%    0x00007f5724aec11c: retq  

ref:

                  [Verified Entry Point]
 13.52%    1.45%    0x00007f1887e66700: mov    %eax,-0x14000(%rsp)
  0.60%    0.37%    0x00007f1887e66707: Push   %rbp
           0.02%    0x00007f1887e66708: sub    $0x30,%rsp         
 13.63%   16.91%    0x00007f1887e6670c: mov    %rsi,%rax     ; load "this"
  0.50%    0.49%    0x00007f1887e6670f: add    $0x30,%rsp
  0.01%             0x00007f1887e66713: pop    %rbp
 39.18%   57.65%    0x00007f1887e66714: test   %eax,0xe3e78e6(%rip)
  0.02%             0x00007f1887e6671a: retq   

[皮肉]それがいかに簡単かを見てください! [/皮肉]

パターンは次のとおりです。質問が単純であればあるほど、もっともらしく信頼できる回答を作成するためにより多くの作業を行う必要があります。

151

referenceおよびmemoryの誤解を解消するために、一部は(@ Mzf)、Java Virtual Machine Specification。に飛び込みましょう。しかし、そこに行く前に、一つのことを明確にする必要があります-オブジェクトをメモリから取得することはできません。そのフィールドのみができます。実際、このような広範な操作を実行するオペコードはありません。

このドキュメントでは、参照をスタックタイプ(結果またはスタックで操作を実行する命令への引数となるように)として定義します。1番目のカテゴリ-単一のスタックWord(32ビット)をとるタイプのカテゴリ。表2.3を参照 A list of Java Stack Types

さらに、仕様に従ってメソッド呼び出しが正常に完了すると、スタックの最上部からポップされた値がメソッドの呼び出し側のスタックにプッシュされます(セクション2.6.4)。

あなたの質問は、実行時間の違いの原因です。第2章の序文の回答:

Java Virtual Machineの仕様の一部ではない実装の詳細は、実装者の創造性を不必要に制約します。たとえば、実行時データ領域のメモリレイアウト、使用されるガベージコレクションアルゴリズム、 Java Virtual Machine命令の内部最適化(たとえば、マシンコードへの変換など)は、実装者の裁量に任されています。

言い換えると、参照の使用に関するパフォーマンスのペナルティなどが論理的な理由でドキュメントに記載されていないため(最終的にはintまたはfloatが単なるスタックWordになります)、実装のソースコードを検索するか、まったく見つけないままにしておきます。

ある程度、実際に実装を常に非難すべきではありません。答えを探すときに手がかりとなるいくつかの手がかりがあります。 Javaは、数値と参照を操作するための個別の命令を定義します。参照操作命令は、aで始まります(例astorealoadまたはareturn)および参照を操作できる唯一の命令であり、特にareturn´sの実装に関心がある場合があります。

6
user35443