奇妙なパフォーマンスの原因を見つける

はじめに



最後に、Javaバイトコードを詳細に研究するのに役立ち、ほとんどすぐに興味深い疑問が頭に浮かびました。 そこには何もしないNOP命令があります。 それでは、この「何も」はパフォーマンスにどのように影響しますか? 実際、これを研究するプロセスはポストで説明されています。



免責事項



ストーリー自体は、まず第一に、それが実際にどのように機能するかに関するものではなく、パフォーマンスを測定するときに注意すべきエラーの種類に関するものです。



ツール



主なことから始めましょう:すべての測定がどのように実行されたか。 コードを生成するために、 ASMライブラリが使用され、ベンチマーク自体-JMHが作成されました。



リフレクションを使用しないために、小さなインターフェイスが作成されました。

public interface Getter { int get(); }
      
      







次に、 getメソッドを実装するクラスが生成されました。

  public get()I NOP ... NOP LDC 20 IRETURN
      
      





任意の数のメモを挿入できます。



完全なジェネレーターコード
 public class SimpleGetterClassLoader extends ClassLoader { private static final String GENERATED_CLASS_NAME = "other.GeneratedClass"; private static final ClassLoader myClassLoader = new SimpleGetterClassLoader(); @SuppressWarnings("unchecked") public static Getter newInstanceWithNOPs(int nopCount) throws Exception { Class<?> clazz = Class.forName(GENERATED_CLASS_NAME + "_" + nopCount, false, myClassLoader); return (Getter) clazz.newInstance(); } @NotNull @Override protected Class<?> findClass(@NotNull String name) throws ClassNotFoundException { if (!name.startsWith(GENERATED_CLASS_NAME)) throw new ClassNotFoundException(name); int nopCount = Integer.parseInt(name.substring(GENERATED_CLASS_NAME.length() + 1)); ClassWriter cw = new ClassWriter(0); cw.visit(V1_5, ACC_PUBLIC, name.replace('.', '/'), null, getInternalName(Object.class), new String[]{getInternalName(Getter.class)}); { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, getInternalName(Object.class), "<init>", "()V"); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); } { MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "get", "()I", null, null); mv.visitCode(); for (int i = 0; i < nopCount; i++) { mv.visitInsn(NOP); } mv.visitLdcInsn(20); mv.visitInsn(IRETURN); mv.visitMaxs(1, 1); mv.visitEnd(); } cw.visitEnd(); byte[] bytes = cw.toByteArray(); return defineClass(name, bytes, 0, bytes.length); } }
      
      







ベンチマーク
 @State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.MICROSECONDS) public class Bench { private Getter nop_0; private Getter nop_10; ... @Setup public void setup() throws Exception { nop_0 = newInstanceWithNOPs(0); nop_10 = newInstanceWithNOPs(10); ... } @GenerateMicroBenchmark public int nop_0() { return nop_0.get(); } @GenerateMicroBenchmark public int nop_10() { return nop_10.get(); } ...
      
      









真実の探求



最初に2つのテストが開始されました:ノブなしと2000年以降。



 Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 838,753 48,962 ops/us b.Bench.nop_2000 thrpt 5 298,428 7,965 ops/us
      
      





そしてすぐに、私は非常に強力な結論を出しました。「愚かなJITは足を切り落とすのではなく、足を機械に変換します。」

専門家への質問:
これが本当だった場合、測定結果は同様ですか? それとも完全に異なるものがありますか?


しかし、これはそれでも仮説であり、私は本当にそれをテストしたかったのです。 最初は、これらのメソッドが実際にJITによってコンパイルされていると確信し、次に何を見ました。 当然、アセンブラは完全に同一でした。 そして、私は何かを理解していないことに気付きました。 実行可能コードはまったく同じであり、パフォーマンスは2.5倍異なります。 変です。



それから私は本当に依存のタイプを見たいと思いました。

 Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 813,010 71,510 ops/us b.Bench.nop_2000 thrpt 5 302,589 12,360 ops/us b.Bench.nop_10000 thrpt 5 0,268 0,017 ops/us
      
      





隠された知識
この測定は一般に豪華です。 3ポイント、すべて異なるシーケンスから。


ここで別に注意する価値があるのは、新しいポイントについては、コンパイルが発生するか出力で何が起こるかに関係なく、私はまったく見なかったことです。 すべてが0 / 2kと同じであると自動的に想定されます。 それは間違いでした。



私はこれを見て、次の広範囲にわたる結論を出しました:「依存は非常に強く非線形です。」 しかし、もっと重要なのは、この場所で、私は本当のことは結び目自体ではなく、メソッドのサイズにあると疑い始めました。



次の考えは、仮想メソッドがあるということでした。つまり、それらは仮想メソッドのテーブルに格納されるということです。 たぶん、テーブル自体はサイズに敏感ですか? 検証のために、コードを静的メソッドに単純に転送しましたが、もちろん何も変わりませんでした。

専門家への質問2
これは完全に愚かだと思いましたか? それとも彼女には賢明な何かがありましたか?




さらに、誤解から、メソッドのサイズが何であるかを見ることが有用でした。 答えはopenjdkのソースで見つかりました:

  develop(intx, HugeMethodLimit, 8000, \ "Don't compile methods larger than this if " \ "+DontCompileHugeMethods")
      
      





興味深いことに、2kと10kの間だけです。 メソッドのサイズを計算してみましょう。「return 20」で3バイト、7997が残ります。

 Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 797,376 12,998 ops/us b.Bench.nop_2000 thrpt 5 306,795 0,243 ops/us b.Bench.nop_7997 thrpt 5 303,314 7,161 ops/us b.Bench.nop_7998 thrpt 5 0,335 0,001 ops/us b.Bench.nop_10000 thrpt 5 0,269 0,000 ops/us
      
      





推測すると、この境界線は明確です。 8000バイトまで何が起こるかを理解することは残っています。 ポイントを追加する:

 Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 853,499 61,847 ops/us b.Bench.nop_10 thrpt 5 845,861 112,504 ops/us b.Bench.nop_100 thrpt 5 867,068 20,681 ops/us b.Bench.nop_500 thrpt 5 304,116 1,665 ops/us b.Bench.nop_1000 thrpt 5 299,295 8,745 ops/us b.Bench.nop_2000 thrpt 5 306,495 0,578 ops/us b.Bench.nop_7997 thrpt 5 301,322 7,992 ops/us b.Bench.nop_7998 thrpt 5 0,335 0,005 ops/us b.Bench.nop_10000 thrpt 5 0,269 0,004 ops/us b.Bench.nop_25000 thrpt 5 0,105 0,007 ops/us b.Bench.nop_50000 thrpt 5 0,053 0,001 ops/us
      
      





ここで最初に喜ばれることは、jitが切断された後、線形関係が非常に明確に見えることです。 これは、私たちの期待とまったく同じです。なぜなら、 各NOPを明示的に処理する必要があります。



次に注目されるのは、最大8kは1種類の依存関係ではなく、2つの定数だけであるという強い感覚です。 さらに5分間の手動バイナリ検索で、境界が見つかりました。

 Benchmark Mode Samples Mean Mean error Units b.Bench.nop_0 thrpt 5 805,466 10,074 ops/us b.Bench.nop_10 thrpt 5 862,027 4,756 ops/us b.Bench.nop_100 thrpt 5 861,462 9,881 ops/us b.Bench.nop_322 thrpt 5 863,176 22,385 ops/us b.Bench.nop_323 thrpt 5 303,677 5,130 ops/us b.Bench.nop_500 thrpt 5 299,368 11,143 ops/us b.Bench.nop_1000 thrpt 5 302,884 3,373 ops/us b.Bench.nop_2000 thrpt 5 306,682 3,598 ops/us b.Bench.nop_7997 thrpt 5 301,457 4,209 ops/us b.Bench.nop_7998 thrpt 5 0,337 0,001 ops/us b.Bench.nop_10000 thrpt 5 0,268 0,004 ops/us b.Bench.nop_25000 thrpt 5 0,107 0,002 ops/us b.Bench.nop_50000 thrpt 5 0,053 0,000 ops/us
      
      





ほとんどすべて、それがどんな種類の境界であるかを理解することは残っています。 計算してみましょう:3 + 322 ==325。どのような魔法325を探しています特定のキー-XX:FreqInlineSizeを見つけます

FreqInlineSizeは、最新の64ビットLinuxでは325です


ドックからの説明:

インライン化される頻繁に実行されるメソッドのバイトコード命令の最大数を指定する整数。




やった! 最後に、すべてが一緒になりました。 全体として、パフォーマンスがメソッドのサイズに依存していることがわかりました(もちろん、「他のすべてのものは等しい」)。

1. JIT +インライン

2. JIT

3.正直な解釈



おわりに



冒頭で述べたように、注意すべき主なことは実際の行動ではありません。 それは非常に些細なことが判明し、ドックに記載されていると確信しています(まだ読んでいませんが、わかりません)。 私の主なメッセージは、常識を信頼することは非常に重要であり、測定結果が少なくともそれと矛盾している場合、または単に理解できないと思われる場合は、すべてを確実に確認して再確認する必要があります。



誰かがこの投稿を面白いと思ったことを願っています。



PS



私は常にバイト数で8000と325の両方をカウントしました。 包括的でない指示でこれを行う必要があったようです。



専門家への質問3
なぜちょうど325と8000なのか? これらの乱数はありますか、またはそれらの背後に何かがありますか?



All Articles