「ウェブスケール」という用語は最近誇大宣伝されており、人々はアプリケーションアーキテクチャを拡大することにより、システムをより「幅広いドメイン」にしています。しかし、完全なドメインとは何ですか?または、ドメイン全体を確保する方法は?
最も誇大宣伝されたスケーリング負荷は、最も人気のあるドメインです。たとえば、単一のユーザーアクセスをサポートするシステムは、10、100、または100万人のユーザーアクセスをサポートすることもできます。理想的には、私たちのシステムはできるだけ「ステートレス」を維持する必要があります。状態が存在する必要がある場合でも、ネットワークのさまざまな処理端子で変換および送信できます。負荷がボトルネックになると、遅延がないかもしれません。したがって、単一のリクエストでは、50〜100ミリ秒かかることは許容できます。これはスケーリングアウトと呼ばれます。
拡張機能は、完全なドメインの最適化において完全に異なって機能します。たとえば、1つのデータが正常に処理されることを保証するアルゴリズムは、10、100、または100万のデータを正常に処理することもできます。イベントの複雑さ(大きなOシンボル)は、このメトリックタイプが実行可能かどうかに関係なく、最良の説明です。レイテンシは、パフォーマンススケーリングキラーです。同じマシンですべての操作を処理するために最善を尽くします。これはスケールアップと呼ばれます。
パイが空から落ちる可能性がある場合(もちろんこれは不可能です)、水平方向の膨張と垂直膨張を組み合わせることができるかもしれません。ただし、今日は、効率を向上させるための以下の簡単な方法を紹介するだけです。
Java 7のForkjoinpoolとJava 8の並列データストリーム(ParallelStream)の両方が、並列処理に役立ちます。すべてのプロセッサが同じメモリにアクセスできるため、マルチコアプロセッサにJavaプログラムを展開するときに特に明白です。
したがって、この並列処理の基本的な利点は、ネットワーク全体のさまざまなマシンのスケーリングと比較して、ほぼ完全にレイテンシを排除することです。
ただし、並列処理の効果に混乱しないでください!次の2つのポイントに留意してください。
アルゴリズムの複雑さを減らすことは、間違いなくパフォーマンスを改善する最も効果的な方法です。たとえば、HashmapインスタンスLookup()メソッドの場合、イベントの複雑さOまたは空間の複雑さO(1)が最速です。しかし、この状況はしばしば不可能であり、簡単にそれを達成することはできません。
アルゴリズムの複雑さを減らすことができない場合は、アルゴリズムのキーポイントを見つけてメソッドを改善することで、パフォーマンスを改善することもできます。次のアルゴリズム図があるとします。
このアルゴリズムの全体的な時間の複雑さはO(N3)であり、別のアクセス順序で計算された場合、複雑さはO(N X O X P)であると結論付けることもできます。とにかく、このコードを分析すると、いくつかの奇妙なシナリオがあります。
生産データの参照がなければ、「ハイオーバーヘッド操作」を最適化する必要があるという結論を簡単に導き出すことができます。しかし、私たちが行った最適化は、配信された製品に影響を与えませんでした。
最適化されたゴールデンルールは、次のものにすぎません。
それがすべて理論のためです。問題が正しい分岐で発生することがわかったと仮定すると、製品の単純な処理は、大量の時間のために応答を失う可能性があります(N、O、Pの値が非常に大きいと仮定)、記事に記載されている左分岐の複雑さはO(N3)であることに注意してください。ここで行われた努力は延長することはできませんが、ユーザーの時間を節約し、後で困難なパフォーマンスの改善を遅らせることができます。
Javaのパフォーマンスを改善するための10のヒントを次に示します。
1。StringBuilderを使用します
StingBuilderは、デフォルトでJavaコードで使用する必要があり、 +演算子を避ける必要があります。たぶん、あなたは次のようなStringBuilderの構文砂糖について異なる意見を持っているでしょう。
文字列x = "a" + args.length + "b";
次のようにコンパイルされます
0 New Java.lang.StringBuilder [16] 3 Dup4 LDC <String "a"> [18] 6 Invokespecial Java.lang.StringBuilder(Java.Lang.String)[20] 9 ALOAD_0 [Args] 10 ArrayLength11 InvokeVirtual Java.lang.StringBuilder.AppEnd(Int): Java.lang.StringBuilder [23] 14 LDC <String "B"> [27] 16 InvokeVirtual Java.lang.StringBuilder.Append(java.lang.String):Java.lang.StringBuilder [29] 19 Invokevirtual Java.lang.StringBuilder. [32] 22 store_1 [x]
しかし、正確に何が起こったのでしょうか?次の文字列を改善するには、次のセクションを使用する必要がありますか?
string x = "a" + args.length + "b"; if(args.length == 1)x = x + args [0];
これで、2番目のStringBuilderを使用しましたが、ヒープに余分なメモリを消費しませんが、GCに圧力をかけます。
StringBuilder X = new StringBuilder( "a"); x..append(args.length); x.append( "b"); if(args.length == 1); x.append(args [0]);
上記の例では、Javaコンパイラに頼ってインスタンスを暗黙的に生成する場合、コンパイルされた効果は、StringBuilderインスタンスが使用されるかどうかとは何の関係もありません。 NOPEブランチでは、各CPUサイクルに費やされた時間がGCに費やされたり、StringBuilderにデフォルトスペースを割り当てたりして、N X O X P時間を無駄にしています。
一般的に言えば、StringBuilderの使用は、 +演算子を使用するよりも優れています。可能であれば、Stringが追加のリソースを消費するため、複数のメソッドに参照を渡す必要がある場合は、StringBuilderを選択します。 JOOQは、複雑なSQLステートメントを生成するときにこの方法を使用します。抽象的構文ツリーのSQLパススルー全体で使用される弦楽剤は1つだけです。
さらに悲劇的なのは、StringBufferをまだ使用している場合、StringBufferの代わりにStringBuilderを使用することです。結局のところ、文字列を同期する必要がある場合は、実際には多くありません。
定期的な表現は、人々が迅速かつ簡単であるという印象を人々に与えます。しかし、NOPEブランチで正規表現を使用することは最悪の決定です。計算集約型コードで正規表現を使用する必要がある場合は、少なくともパターンをキャッシュして、パターンを繰り返しコンパイルしないようにします。
静的最終パターン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)== '。'){part [part] = iPaddress.substring(offset、offset、i+1);パート+1);パート+1);パート+1);上記のコードは、早期最適化が無意味であることも示しています。 Split()メソッドと比較しても、このコードは保守性が低くなります。
チャレンジ:スマートフレンドはより速いアルゴリズムを思いつくことができますか?
正規表現は非常に便利ですが、使用すると価格も支払う必要があります。特に、NOPEブランチの深い場合は、どんな犠牲を払っても正規表現を使用しないようにする必要があります。また、string.replaceall()やstring.split()などの正規表現を使用するさまざまなJDK文字列メソッドにも注意してください。 Apache Commons Langなどのより人気のある開発ライブラリを使用して、文字列操作を実行することを選択できます。
この提案は、一般的な機会には適用されませんが、NOPEブランチの深いシナリオにのみ適用されます。それでも、あなたは何かを理解する必要があります。 Java 5フォーマットループライティング方法は非常に便利であるため、次のような内部ループ方法を忘れることができます。
for(文字列値:文字列){//ここで何か便利なことをする}コードがこのループに実行されるたびに、文字列変数が反復可能である場合、コードは自動的にイテレーターのインスタンスを作成します。 ArrayListを使用している場合、仮想マシンは3つの整数タイプメモリをヒープ上のオブジェクトに自動的に割り当てます。
プライベートクラスITRはIterator <e> {int cursor; int lastret = -1; int expectsModCount = modCount; // ...また、次の同等のループ法を使用して、上記のループを置き換えることもできます。これは、スタックに整形手術を「浪費」するだけです。これは非常に費用対効果が高いです。
int size = strings.size(); for(int i = 0; i <size; i ++){string value:strings.get(i); //ここで何か便利なことをしてください}ループ内の文字列の値があまり変化しない場合は、配列を使用してループを実装することもできます。
for(string value:stringarray){//ここで何か役に立つことをします}簡単な読み取りと執筆の観点から、またはAPIデザイン、イテレーター、反復可能なインターフェイス、およびforeachループの観点からでも非常に便利です。しかし、犠牲を払って、それらを使用する場合、各ループチャイルドのヒープに追加のオブジェクトが作成されます。ループを何度も実行する場合は、無意味なインスタンスの生成を避けるように注意してください。基本的なポインターループ法を使用して、上記のイテレーター、反復可能なインターフェイス、およびforeachループを置き換えることをお勧めします。
上記に反対しているいくつかの意見(特にイテレーターをポインターに置き換える)については、詳細についてはRedditで説明します。
いくつかの方法は高価です。 NOPEブランチを例にとると、関連する葉の方法については言及しませんでしたが、これは見つかります。 JDBCドライバーは、resultset.wasnull()メソッドの返品値を計算するためにすべての難しさを克服する必要があるとします。私たちが自分自身を実装するSQLフレームワークは、次のようになるかもしれません:
if(type == integer.class){result =(t)wasnull(rs、integer.valueof(rs.getint(index)));} //} //、そして... static final <t> t wasnull(resultet rs、t values)はsqlexception {return rs.wasnull()をスローします。 null:value;}上記のロジックでは、resultset.wasnull()メソッドは、結果セットからint値が取得されるたびに呼び出されますが、getInt()の方法は次のように定義されます。
返品タイプ:変数値。 SQLクエリの結果がnullの場合、0を返します。
したがって、それを改善するためのシンプルで効果的な方法は次のとおりです。
static final <t extends number> t wasnull(results rs、t values)throws sqlexception {value == null ||(value.intvalue()== 0 && rs.wasnull())? null:value;}これは簡単です。
キャッシュメソッドは、葉のノードに高いオーバーヘッドメソッドを置き換えるための呼び出し、またはメソッド慣習が許可されている場合は高オーバーヘッドメソッドを呼び出すことを避けます。
上記では、JOOQの例で多数のジェネリックを使用して、BYTE、SHORT、INT、LONG LAPPERクラスを使用します。しかし、少なくともジェネリックは、Java 10またはValhallaプロジェクトに特化するまで、コード制限ではありません。それは次の方法に置き換えることができるからです。
//ヒープ整数I = 817598に保存されています。
...このように書く場合:
// Stack int i = 817598に保存します。
配列を使用すると、事態が悪化する可能性があります。
// 3つのオブジェクトがヒープ整数で生成されました[] i = {1337、424242};...このように書く場合:
//ヒープで1つのオブジェクトのみが生成されます[] i = {1337、424242};私たちがNOPEブランチの奥深くにいるときは、パッケージクラスの使用を避けるために最善を尽くしてください。これを行うことの欠点は、GCに大きなプレッシャーをかけることです。 GCは、パッケージングクラスによって生成されたオブジェクトをクリアするのに非常に忙しいでしょう。
したがって、効果的な最適化方法は、基本的なデータ型、固定長い配列を使用し、一連のスプリット変数を使用して、配列内のオブジェクトの位置を識別することです。
LGPLプロトコルに続くTROVE4Jは、Javaコレクションクラスライブラリであり、シェーピングアレイint []よりも優れたパフォーマンス実装を提供します。
次の例外はこのルールに関するものです。なぜなら、ブールとバイトのタイプでは、JDKがキャッシュ方法を提供できるようにするには十分ではないからです。このように書くことができます:
ブールa1 = true; // ... Syntax Sugar for:boolean a2 = boolean.valueof(true); byte b1 =(byte)123; // ...構文砂糖:byte b2 = byte.valueof((byte)123);
他の基本的なタイプの整数には、Char、Short、Int、Longなどの同様の状況もあります。
コンストラクターを呼び出すときにこれらの整数プリミティブタイプを自動的にボックスしたり、theType.valueof()メソッドを呼び出したりしないでください。
また、ヒープで作成されていないインスタンスを取得したくない限り、ラッパークラスのコンストラクターを呼び出しないでください。これを行うことの利点は、同僚に巨大なエイプリルフールの日のジョークを与えることです。
もちろん、オフヒープ機能ライブラリを体験したい場合は、最も楽観的なローカルなソリューションではなく、多くの戦略的決定と混合される可能性があります。 Peter LawreyとBen Cottonが書いた非HEAPストレージに関する興味深い記事については、[OpenJDKとHashmap]をクリックしてください - 退役軍人に安全に(非HEAPストレージ!)新しいテクニックをマスターしてください。
今日、Scalaのような機能的なプログラミング言語は再帰を奨励しています。再帰は通常、個別に最適化された個人に分解できるテール再回帰を意味するためです。使用するプログラミング言語がサポートできる方が良いでしょう。しかし、それでも、アルゴリズムのわずかな調整により、テールの再帰が通常の再帰に変わるように注意してください。
コンパイラがこれを自動的に検出できることを願っています。そうしないと、いくつかのローカル変数でできることについて、多くのスタックフレームを無駄にしていたでしょう。
このセクションでは、再帰の代わりにNOPEブランチで可能な限り反復することを除いて、言うことは何もありません。
キー価値のペアで保存されたマップを反復したい場合は、次のコードの正当な理由を見つける必要があります。
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();}NOPEブランチを使用する場合、MAPを使用する必要があります。なぜなら、O(1)の時間複雑さを持っていると思われる多くのアクセス操作は、実際に一連の操作で構成されているからです。そして、アクセス自体は無料ではありません。少なくとも、MAPを使用する必要がある場合は、EntrySet()メソッドを使用して反復する必要があります。このようにして、Map.Entryのインスタンスにのみアクセスしたいと考えています。
キー価値のペアの形でマップを反復する必要がある場合は、EntrySet()メソッドを必ず使用してください。
8. enumsetまたはenummapを使用します
設定マップを使用する場合など、場合によっては、マップに保存されているキー値が事前に存在する場合があります。このキー値が非常に小さい場合は、一般的に使用するハッシュセットまたはハッシュマップを使用する代わりに、enumsetまたはenummapの使用を検討する必要があります。次のコードは明確な説明を提供します。
プライベートトランジェントオブジェクト[] vals; public v put(k key、v value){// ... int index = key.ordinal(); vals [index] = masknull(value); // ...}前のコードの重要な実装は、ハッシュテーブルの代わりに配列を使用することです。特に、新しい値をマップに挿入する場合、各列挙タイプのコンパイラによって生成される一定のシーケンス番号を取得するだけです。グローバルマップ構成がある場合(たとえば、1つのインスタンスのみ)、Enummapはアクセス速度を上げるという圧力の下でHashmapよりも優れたパフォーマンスを達成します。その理由は、EnummapがHashMapよりも少し少ないヒープメモリを使用し、HashMapは各キー値のHashCode()メソッドとEquals()メソッドを呼び出すためです。
EnumとEnummapは親しい友人です。列挙のような構造と同様のキー値を使用する場合、これらのキー値を列挙型として宣言し、それらを列挙キーとして使用することを検討する必要があります。
Enummapを使用できない場合、少なくともHashCode()およびEquals()メソッドを最適化する必要があります。高オーバーヘッドequals()メソッドへの不必要な呼び出しを防ぐため、適切なハッシュコード()メソッドが必要です。
各クラスの継承構造では、容易に受け入れられる簡単なオブジェクトが必要です。 Jooqのorg.jooq.tableがどのように実装されているかを見てみましょう。
最もシンプルで最速のハッシュコード()実装方法は次のとおりです。
// abstractTable一般テーブルの基本的な実装:@overridepublic int hashcode(){// [#1938]標準QueryPartsと比較して、これはより効率的なHashCode()実装です。 return name.hashcode();}名前はテーブル名です。テーブル名は通常データベースでは一意であるため、スキーマやその他のテーブル属性を考慮する必要さえありません。変数名は文字列であり、それ自体がすでにHashCode()値をキャッシュしています。
AbstractQueryPartから継承されたAbstractTableは、抽象的構文ツリー要素の基本的な実装であるため、このコードのコメントは非常に重要です。通常の抽象的な構文ツリー要素には属性がないため、HashCode()メソッドの実装を最適化することについてファンタジーを持つことはできません。上書きのhashcode()メソッドは次のとおりです。
// AbstractQueryPart一般的な抽象的構文ツリー基本実装:@overridepublic int hashcode(){//これは実用的なデフォルトの実装です。 //特定の実装サブクラスは、この方法をオーバーライドしてパフォーマンスを改善する必要があります。 return create()。renderinlined(this).hashcode();}言い換えれば、SQLレンダリングワークフロー全体がトリガーされ、通常の抽象的構文ツリー要素のハッシュコードが計算されます。
equals()メソッドはさらに興味深いものです。
// AbstractTableの一般的な一般テーブルの基本的な実装:@OverridePublic Boolean Equals(Object That){if(this == that){return true;} // [#2144]ハイオーバーヘッドAbstractQueryPart.Equals()メソッドを呼び出す前に、//オブジェクトが等しくないかどうかを早期に知ることができます。 if(abstracttableのinstance){if(stringutils.equals(name、(((abstracttable <?>)that).name))))){return super.equals(that);} return false;} return false;}まず、次の場合、equals()メソッドを早すぎる(nopeだけでなく)使用しないでください。
注:互換性のあるタイプを確認するには早すぎるInstanceOfを使用する場合、次の条件には実際には引数== nullが含まれます。これについては、以前のブログですでに説明しています。10個のExquisite Java Coding Best Practicesを参照してください。
上記の状況の比較が終わった後、いくつかの結論を引き出すことができるはずです。たとえば、JOOQのtable.equals()メソッドは、2つのテーブルが同じかどうかを比較するために使用されます。特定の実装タイプに関係なく、それらは同じフィールド名を持っている必要があります。たとえば、次の2つの要素は同じではありません。
着信パラメーターがインスタンス自体(これ)に等しいかどうかを簡単に判断できれば、結果がfalseの場合は操作を放棄できます。返品結果が真である場合、親クラスのスーパー実装をさらに判断できます。比較されたオブジェクトのほとんどが等しくない場合、CPUの実行時間を節約するために、できるだけ早くメソッドを終了できます。
一部のオブジェクトは、他のオブジェクトよりも高い類似性を持っています。
JOOQでは、ほとんどのテーブルインスタンスはJOOQのコードジェネレーターによって生成され、これらのインスタンスのequals()メソッドが深く最適化されています。他の多数のテーブルタイプ(派生テーブル)、テーブル値関数、配列テーブル、結合されたテーブル、ピボットテーブル、一般的なテーブル式などは、equals()メソッドの基本的な実装を維持します。
最後に、Javaだけでなく、すべての言語に適用できる別の状況があります。さらに、以前に調査したNOPEブランチは、O(N3)からO(N Log N)への理解にも役立ちます。
残念ながら、多くのプログラマーは、単純なローカルアルゴリズムを使用して問題を検討しています。問題を段階的に解決するために使用されます。これは「はい/または」命令の形です。このプログラミングスタイルは、純粋な命令プログラミングからオブジェクト指向プログラミングに機能的なプログラミングに変換するときに「大きな画像」をモデル化するのが簡単ですが、これらのスタイルにはSQLとRにのみ存在するものがありません。
宣言的なプログラミング。
SQLでは、アルゴリズムの影響を考慮せずにデータベースに必要な効果を宣言できます。データベースは、制約、キー、インデックスなど、データ型に従って最適なアルゴリズムを採用できます。
理論的には、SQLとリレーショナル計算の後、最初に基本的なアイデアがありました。実際には、SQLベンダーは、過去数十年にわたってオーバーヘッドベースの効率的なオプティマイザー(コストベースのオプティマイザー)を実装してきました。その後、2010年バージョンでは、ついにSQLのすべての可能性を発見しました。
ただし、SETメソッドを使用してSQLを実装する必要はありません。すべての言語とライブラリは、セット、コレクション、バッグ、リストをサポートしています。セットを使用することの主な利点は、コードを簡潔で明確にすることができることです。たとえば、次の記述方法:
Somesetはsomothersetを交差させます
その代わり
// javaの以前の執筆方法8 set result = new Hashset(); (object候補:someset)if(sometherset.contains(候補))result.add(候補); // java 8が使用されていても、それはあまり役に立たない場合でも、someset.stream()。フィルター(sometherset :: contains).collect(collectors.toset());
一部の人々は、機能的なプログラミングとJava 8について異なる意見を持っている場合があります。しかし、この見解は必ずしも真実ではありません。 Imperative Java 7ループをJava 8のストリームコレクションに変換できますが、同じアルゴリズムを使用しています。しかし、SQLスタイルの表現は異なります:
Somesetはsomothersetを交差させます上記のコードは、異なるエンジンで1000の異なる実装を持つことができます。今日私たちが勉強しているのは、交差操作を呼び出す前に、2つのセットを自動的に列挙に変換することです。基礎となるStream.Parallel()メソッドを呼び出すことなく、並列交差操作を実行できます。
この記事では、NOPE分岐に関する最適化について説明します。たとえば、高複合性アルゴリズムに深く入ります。 JOOQの開発者として、SQL Generationを最適化させていただきます。
JOOQは、JVMを離れてDBMSに入るときにコンピュータープログラムで呼ばれる最後のAPIであるため、「食物連鎖の底」にいます。食物連鎖の底にあるということは、あらゆるラインがJOOQで実行されるときにn x o x pに時間がかかることを意味します。そのため、できるだけ早く最適化したいと思います。
私たちのビジネスロジックは、NOPEブランチほど複雑ではないかもしれません。ただし、基本的なフレームワークは非常に複雑である場合があります(ローカルSQLフレームワーク、ローカルライブラリなど)。したがって、今日述べた原則に従って、Javaミッションコントロールまたはその他のツールを使用して、最適化が必要な領域があるかどうかを確認する必要があります。
オリジナルリンク:jaxenter翻訳:importnew.com-常にロード翻訳リンク:http://www.importnew.com/16181.html