Javaのシンプルなパフォーマンス最適化トップ10

Java 7ForkJoinPoolとJava8のParallelStreamは、並列化に役立ちます。これは、Javaプログラムをマルチコアプロセッサマシンにデプロイするときに非常に役立ちます。ネットワーク上の異なるマシン間でのスケーリングと比較したこの並列処理の利点は、すべてのコアが同じメモリにアクセスできるため、遅延の影響をほぼ完全に排除できることです。しかし、並列効果にだまされないでください!次の2つの点に注意してください。

  • 並列処理はコアを使い果たします。これはバッチ処理には最適ですが、非同期サーバー(HTTPなど)にとっては悪夢です。過去数十年の間、私たちは正当な理由でシングルスレッドサーブレットモデルを使用してきました。したがって、並列処理はスケールアップ時にのみ役立ちます。

  • 並列処理は、アルゴリズムのBigO表記には影響しません。アルゴリズムがO(n log n)であり、そのアルゴリズムをcコアで実行させた場合でも、cはアルゴリズムの複雑さ定数の些細な要素であるため、O(n log n / c)アルゴリズムを使用できます。壁時計時間を節約できますが、複雑さを軽減することはできません。

もちろん、パフォーマンスを向上させる最善の方法は、アルゴリズムの複雑さを軽減することです。キラーは、もちろん、O(1)または準O(1)を達成することです。たとえば、HashMapルックアップです。しかし、それは簡単なことは言うまでもなく、常に可能であるとは限りません。複雑さを軽減できない場合、本当に重要な場所でアルゴリズムを微調整する場合、適切な場所を見つけることができれば、それでも多くのパフォーマンスを得ることができます。次のアルゴリズムの視覚的表現を想定します。

写真

アルゴリズムの全体的な複雑さは、または1桁を扱っている場合です。ただし、このコードを分析すると、興味深いシナリオが見つかる場合があります。O(N3)O(N x O x P)

  • 開発ボックスでは、開発サンプルデータのPとOの値が小さいため、左側のブランチ(N-> M->ヘビーオペレーション)がプロファイラーに表示される唯一のブランチです。

  • ただし、本番環境では、正しいブランチ(N-> O-> P-> Easy OperationまたはNOPE)によって問題が発生します。運用チームは、AppDynamics、DynaTrace、または同様のソフトウェアを使用してこの問題を解決した可能性があります。

本番データがなければ、すぐに結論に達し、「大量の操作」を最適化する可能性があります。本番環境に出荷し、修正は効果がありません。次の事実以外に最適化の黄金律はありません。

  • 適切に設計されたアプリケーションは、最適化が容易です

  • 時期尚早の最適化はパフォーマンスの問題を解決しません。アプリケーションの設計が不十分になり、最適化が困難になります。

理論は十分です。正しいブランチが問題であることがわかったとしましょう。非常に単純な操作は、何度も呼び出されるため(N、O、およびPが大きい場合)、本番環境で失敗する可能性があります。アルゴリズムのリーフノードで避けられない問題があるこの記事を読んでください。これらの最適化は、スケーリングには役立ちません。彼らはあなたが一時的にあなたのクライアントの時間を節約するのを助け、アルゴリズム全体への難しい改善を後でまで延期します!O(N3)Javaで最も簡単な10のパフォーマンス最適化は次のとおりです。

1.StringBuilderを使用します

これは、ほとんどすべてのJavaコードのデフォルトです。+演算子の使用は避けてください。もちろん、これはStringBuilderの単なる構文糖衣であると主張するかもしれません。たとえば、次のようになります。

String x = "a" + args.length + "b";
复制代码

次のように変換されます。

new java.lang.StringBuilder [16]dupldc <String "a"> [18]invokespecial java.lang.StringBuilder(java.lang.String) [20]aload_0 [args]arraylengthinvokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]ldc <String "b"> [27]invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]astore_1 [x]
复制代码

しかし、後でオプションのパーツを使用して文字列を変更する必要がある場合はどうなりますか?

String x = "a" + args.length + "b"; if (args.length == 1)    x = x + args[0];
复制代码

これで、ヒープメモリを不必要に消費してGCに負荷をかけている2番目のStringBuilderができました。代わりにこれを書いてください:

StringBuilder x = new StringBuilder("a");x.append(args.length);x.append("b"); if (args.length == 1);    x.append(args[0]);
复制代码

上記の例では、明示的なStringBuilderインスタンスを使用した場合、またはJavaコンパイラに依存して暗黙的なインスタンスを作成した場合はまったく問題にならない可能性があります。ただし、私たちはNOPEブランチにいることを忘れないでください。CPUサイクルごとに、GCやaStringBuilderのデフォルト容量の割り当てなどの愚かなことに浪費し、N x OxP時間を浪費しています。経験則として、+演算子の代わりに常にaStringBuilderを使用してください。可能であれば、ビルドがより複雑な場合は、StringBuilderの複数のメソッドへの参照を保持してください。SQL AST(抽象構文木)全体を「トラバース」するStringBuilderは1つだけです。大きな叫び声の場合、StringBuffer参照がまだある場合は、それらをStringBuilderに置き換えます。作成している文字列を同期する必要はほとんどありません。

2.正規表現を避けます

正则表达式相对便宜和方便。但是,如果您在 N.O.P.E. 分支中,那么它们就是您能做的最糟糕的事情。如果您绝对必须在计算密集型代码部分使用正则表达式,至少缓存Pattern引用而不是一直重新编译它:

static final Pattern HEAVY_REGEX =    Pattern.compile("(((X)*Y)*Z)*");
复制代码

但是如果你的正则表达式真的很傻

String[] parts = ipAddress.split("\\.");
复制代码

那么你真的最好求助于普通char[]或基于索引的操作。例如,这个完全不可读的循环做同样的事情:

int length = ipAddress.length();int offset = 0;int part = 0;for (int i = 0; i < length; i++) {    if (i == length - 1 ||            ipAddress.charAt(i + 1) == '.') {        parts[part] =            ipAddress.substring(offset, i + 1);        part++;        offset = i + 2;    }}
复制代码

这也说明了为什么你不应该做任何过早的优化。与split()版本相比,这是不可维护的。挑战:读者中聪明的人可能会发现更快的算法。外卖 正则表达式很有用,但它们是有代价的。如果您深陷于N.O.P.E.分支中,则必须不惜一切代价避免使用正则表达式。请注意各种使用正则表达式的 JDK 字符串方法,例如String.replaceAll(), 或String.split(). 请改用Apache Commons Lang之类的流行库来进行字符串操作。

3、不要使用iterator()

现在,此建议实际上不适用于一般用例,而仅适用于N.O.P.E.分支的深层。尽管如此,你应该考虑一下。编写 Java-5 风格的 foreach 循环很方便。您可以完全忘记循环内部,并编写:

for (String value : strings) {    // Do something useful here}
复制代码

但是,每次遇到此循环时,如果strings是一个Iterable,您将创建一个新Iterator实例。如果您使用的是ArrayList,这将ints在您的堆上分配一个 3 的对象:

private class Itr implements Iterator<E> {    int cursor;    int lastRet = -1;    int expectedModCount = modCount;    // ...
复制代码

相反,您可以编写以下等效循环并仅“浪费”堆栈上的单个int值,这非常便宜:

int size = strings.size();for (int i = 0; i < size; i++) {    String value : strings.get(i);    // Do something useful here}
复制代码

……或者,如果您的列表没有真正改变,您甚至可以对它的数组版本进行操作:

for (String value : stringArray) {    // Do something useful here}
复制代码

从可写性和可读性的角度来看,以及从 API 设计的角度来看,迭代器、Iterable 和 foreach 循环都非常有用。但是,它们会在每次迭代时在堆上创建一个小的新实例。如果你多次运行这个迭代,你要确保避免创建这个无用的实例,而是编写基于索引的迭代。

4、不要调用那个方法

有些方法简单昂贵。在我们的N.O.P.E.分支示例中,我们在叶子中没有这样的方法,但您可能有一个。让我们假设您的 JDBC 驱动程序需要经历令人难以置信的麻烦来计算ResultSet.wasNull(). 您自己开发的 SQL 框架代码可能如下所示:

if (type == Integer.class) {    result = (T) wasNull(rs,        Integer.valueOf(rs.getInt(index)));}



// And then...static final <T> T wasNull(ResultSet rs, T value)throws SQLException {    return rs.wasNull() ? null : value;}
复制代码

ResultSet.wasNull() 现在,每次您int从结果集中获得一个时, 都会调用此逻辑。但getInt()合同上写着:

返回:列值;如果值为 SQL NULL,则返回值为 0

因此,对上述内容的一个简单但可能是巨大的改进将是:

static final <T extends Number> T wasNull(    ResultSet rs, T value)throws SQLException {    return (value == null ||           (value.intValue() == 0 && rs.wasNull()))        ? null : value;}
复制代码

所以,这很简单:要点 不要在算法“叶节点”中调用昂贵的方法,而是缓存调用,或者在方法合约允许的情况下避免调用。

5、使用原语和堆栈

上面的例子,它使用了很多泛型,因此被迫使用包装器类型byte, short, int, 和long– 至少在泛型在 Java 10 和项目 Valhalla 中专用之前。但是你的代码中可能没有这个约束,所以你应该采取一切措施来替换:

// Goes to the heapInteger i = 817598;
复制代码

这样:

// Stays on the stackint i = 817598;
复制代码

使用数组时情况会变得更糟:

// Three heap objects!Integer[] i = { 1337, 424242 };
复制代码

这样:

// One heap object.int[] i = { 1337, 424242 };
复制代码

当您深入到N.O.P.E.分支时,您应该非常小心使用包装器类型。很有可能你会给你的 GC 造成很大的压力,它必须一直在清理你的烂摊子。一个特别有用的优化可能是使用一些原始类型并创建它的大型一维数组,以及几个分隔符变量来指示您的编码对象在数组上的确切位置。trove4jint[]是一个优秀的原始集合库,它比你的平均水平要复杂一些,它与 LGPL 一起提供。例外 此规则有一个例外:和booleanbyte很少有足够的值完全被 JDK 缓存。你可以写:

Boolean a1 = true; // ... syntax sugar for:Boolean a2 = Boolean.valueOf(true); Byte b1 = (byte) 123; // ... syntax sugar for:Byte b2 = Byte.valueOf((byte) 123);
复制代码

对于其他整数基本类型的低值也是如此,包括char, short, int, long。但仅当您自动装箱或调用TheType.valueOf()时,才不会在调用构造函数时!

永远不要在包装类型上调用构造函数,除非你真的想要一个新实例

这个事实也可以帮助你为你的同事写一个复杂的愚人节笑话 堆 外 当然,你可能还想尝试堆外库,尽管它们更多的是战略决策,而不是本地优化。

6、避免递归

像 Scala 这样的现代函数式编程语言鼓励使用递归,因为它们提供了将尾递归算法优化回迭代算法的方法。如果你的语言支持这样的优化,你可能没问题。但即便如此,算法的最轻微变化也可能会产生一个分支,阻止你的递归是尾递归的。希望编译器会检测到这一点!否则,您可能会浪费大量堆栈帧,而这些堆栈帧可能仅使用几个局部变量就可以实现。当你深入N.O.P.E.分支时,总是更喜欢迭代而不是递归

7、使用 entrySet()

当您想要遍历 aMap并且需要键和值时,您必须有充分的理由编写以下内容:

for (K key : map.keySet()) {    V value : map.get(key);}
复制代码

而不是以下内容:

for (Entry<K, V> entry : map.entrySet()) {    K key = entry.getKey();    V value = entry.getValue();}
复制代码

当你在N.O.P.E.分支时,无论如何你都应该警惕地图,因为大量的O(1)地图访问操作仍然是大量的操作。而且访问也不是免费的。但至少,如果您不能没有地图,请使用它entrySet()来迭代它们!无论如何,该Map.Entry实例都在那里,您只需要访问它。entrySet()在 map 迭代过程中同时需要键和值时 始终使用。

8、使用 EnumSet 或 EnumMap

在某些情况下,映射中可能的键的数量是预先知道的——例如在使用配置映射时。如果该数字相对较小,您应该真正考虑使用EnumSetor EnumMap,而不是常规HashSetor HashMap。这很容易通过查看来解释EnumMap.put():

private transient Object[] vals; public V put(K key, V value) {    // ...    int index = key.ordinal();    vals[index] = maskNull(value);    // ...}
复制代码

这个实现的本质是,我们有一个索引值数组,而不是一个哈希表。当插入一个新值时,为了查找映射条目,我们所要做的就是向enum查询它的常数序数,该常数序数是由Java编译器在每个enum类型上生成的。如果这是一个全局配置映射(即只有一个实例),增加的访问速度将帮助EnumMap大大超过HashMap,它可能使用更少的堆内存,但必须在每个键上运行hashCode()和equals()。Enum和EnumMap是非常亲密的朋友。当您使用类似于枚举的结构作为键时,请实际考虑将这些结构作为枚举,并在EnumMap中使用它们作为键。

9、优化你的 hashCode() 和 equals() 方法

如果不能使用EnumMap,至少优化hashCode()和equals()方法。一个好的hashCode()方法是必要的,因为它将防止进一步调用开销大得多的equals(),因为它将为每个实例集生成更多不同的散列桶。在每个类层次结构中,都可能有流行的和简单的对象。hashCode()最简单、最快的实现是这样的:

// AbstractTable, a common Table base implementation: @Overridepublic int hashCode() {     // [#1938] This is a much more efficient hashCode()    // implementation compared to that of standard    // QueryParts    return name.hashCode();}
复制代码

name表名在哪里。我们甚至不考虑表的模式或任何其他属性,因为表名通常在数据库中足够不同。此外,它name是一个字符串,所以它里面已经有一个缓存hashCode()值。注释很重要,因为AbstractTableextends是任何AST(抽象语法树)元素AbstractQueryPart的通用基础实现。通用 AST 元素没有任何属性,因此它不能对优化实现做出任何假设。因此,被覆盖的方法如下所示:hashCode()

// AbstractQueryPart, a common AST element// base implementation: @Overridepublic int hashCode() {    // This is a working default implementation.    // It should be overridden by concrete subclasses,    // to improve performance    return create().renderInlined(this).hashCode();}
复制代码

换句话说,必须触发整个 SQL 渲染工作流来计算一个普通 AST 元素的哈希码。事情变得更有趣equals()

// AbstractTable, a common Table base implementation: @Overridepublic boolean equals(Object that) {    if (this == that) {        return true;    }     // [#2144] Non-equality can be decided early,    // without executing the rather expensive    // implementation of AbstractQueryPart.equals()    if (that instanceof AbstractTable) {        if (StringUtils.equals(name,            (((AbstractTable<?>) that).name))) {            return super.equals(that);        }         return false;    }     return false;}
复制代码

第一件事:总是(不仅在NOPE 分支中)提前中止每个equals()方法,如果:

  • this == argument

  • this "incompatible type" argument

请注意,后一个条件包括argument == null, 如果您instanceof用于检查兼容类型。我们之前在10 Subtle Best Practices when Coding Java中对此进行了博文。现在,在明显情况下尽早中止比较之后,您可能还希望在可以做出部分决定时尽早中止比较。例如,约定Table.equals()是两个表被认为是相等的,它们必须具有相同的名称,而不管具体的实现类型如何。例如,这两项不可能相等:

  • com.example.generated.Tables.MY_TABLE

  • DSL.tableByName("MY_OTHER_TABLE")

如果argument 不能等于this,并且我们可以轻松地检查它,那么让我们这样做并在检查失败时中止。如果检查成功,我们仍然可以从super. 鉴于宇宙中的大多数对象都不相等,我们将通过快捷方式节省大量 CPU 时间。

10、在集合中思考,而不是在单个元素

最后但并非最不重要的一点是,有一件事与 Java 无关,但适用于任何语言。此外,我们将离开N.O.P.E.分支,因为此建议可能只会帮助您从 迁移到,或类似的东西。不幸的是,许多程序员从简单的本地算法的角度来思考。他们正在逐步解决问题,一个分支一个分支,一个循环一个循环,一个方法一个方法。这就是命令式和/或函数式编程风格。虽然在从纯命令式到面向对象(仍然是命令式)再到函数式编程时,对“更大的图景”进行建模变得越来越容易,但所有这些风格都缺乏只有 SQL 和 R 以及类似语言才有的东西:声明式编程。在 SQL 中(我们喜欢它,因为这是O(N3)O(n log n)) 你可以声明你想从你的数据库中得到的结果,而不会对算法产生任何影响。然后,数据库可以考虑所有可用的元数据(例如约束、键、索引等),以找出可能的最佳算法。从理论上讲,这从一开始就是SQL 和关系演算背后的主要思想。使用集合的主要优点是您的算法将变得更加简洁。

而不是:

// Pre-Java 8Set result = new HashSet();for (Object candidate : someSet)    if (someOtherSet.contains(candidate))        result.add(candidate); // Even Java 8 doesn't really helpsomeSet.stream()       .filter(someOtherSet::contains)       .collect(Collectors.toSet());
复制代码

関数型プログラミングとJava8は、より単純でクリーンなアルゴリズムの作成に役立つと主張する人もいるかもしれません。これは必ずしも真実ではありません。命令型のJava-7ループを機能的なJava-8Streamコレクションに変換できますが、それでも同じアルゴリズムを作成しています。SQLのような式の記述は異なります。

SomeSetはSomeOtherSetと交差します

実装エンジンを介して1000通りの方法で実装できます。EnumSet今日学んだように、操作を実行する前に、両方のセットを自動的にINTERSECTに変換するのが賢明です。Stream.parallel()を低レベルで呼び出さなくても、INTERSECTを並列化できるかもしれません。

11.結論

このホワイトペーパーでは、NOPEブランチで実行される最適化、つまり複雑度の高いアルゴリズムの奥深くで実行される最適化について説明します。

  • クエリごとに1つのStringBuilderのみを生成します

  • 私たちのテンプレートエンジンは、正規表現を使用する代わりに実際に文字を解析しています

  • 特にリスナーを反復処理する場合は、可能な限り配列を使用します

  • 呼び出す必要のないJDBCメソッドには近づきません

  • 等…

この記事はグーグル翻訳に基づいており、文章は簡単に調整されていますが、それでも場所によってはぎくしゃくしている可能性があります。興味がある場合は、元のアドレスで読むことができます。

PS:この記事が見つからないようにするには、ブックマークして高く評価して、簡単に閲覧して見つけることができるようにします。

おすすめ

転載: juejin.im/post/7078286560034029575
おすすめ