41

状況:

私はLZF圧縮アルゴリズムのpure-java実装を最適化しています。これには、ハッシュと比較のための多くのbyte[]アクセスと基本的なint数学が含まれます。圧縮の目標はI/O要件を減らすことであるため、パフォーマンスは非常に重要です。コードはまだクリーンアップされておらず、大幅に再構築されている可能性があるため、コードを投稿していません。

質問:

  • より高速なSSE操作を使用してフォームにJITコンパイルできるようにコードを作成するにはどうすればよいですか?
  • コンパイラが配列境界チェックを簡単に排除できるように、どのように構造化できますか?
  • 特定の数学演算の相対速度に関する幅広い参考資料はありますか(通常の加算/減算に等しくなるために必要な増分/減分の数、シフトの速度、または配列アクセスの速度)?
  • 分岐の最適化にどのように取り組むことができますか?短い本体を持つ多数の条件ステートメント、またはいくつかの長いステートメント、またはネストされた条件を持つ短いステートメントがある方が良いですか?
  • 現在の1.6JVMでは、System.arraycopyがコピーループを打ち負かす前に、いくつの要素をコピーする必要がありますか?

私がすでにしたこと:

時期尚早の最適化で攻撃される前に:基本的なアルゴリズムはすでに優れていますが、Javaの実装は同等のCの2/3未満の速度です。コピーループをSystem.arraycopyに置き換え、ループの最適化に取り組み、 -必要な操作。

私は、パフォーマンスのためにビットをいじったり、バイトをintにパックしたり、シフトやマスキングを多用しています。

法的な理由から、同様のライブラリの実装を確認することはできません。また、既存のライブラリのライセンス条項は制限が厳しすぎて使用できません。

良い(受け入れられた)答えの要件:

  • 受け入れられない答え:「これはより速い」とは、その量と理由の説明がない場合、またはJITコンパイラでテストされていない場合です。
  • 境界線の回答:Hotspot1.4より前では何もテストされていません
  • 基本的な答え:一般的なルールと、コンパイラレベルで高速である理由と、おおよそどれだけ高速であるかについての説明を提供します
  • 良い答え:デモンストレーション用のコードのサンプルをいくつか含めてください
  • 優れた回答: JRE1.5と1.6の両方でベンチマークを実行する
  • 完璧な答え: HotSpotコンパイラに取り組んだ人によるもので、使用する最適化の条件と、通常の速度を完全に説明または参照できます。HotSpotによって生成されたJavaコードとサンプルアセンブリコードが含まれる場合があります。

また、ホットスポットの最適化と分岐のパフォーマンスの要点を詳しく説明しているリンクがあれば、それを歓迎します。私はバイトコードについて十分に知っているので、ソースコードレベルではなくバイトコードでパフォーマンスを分析するサイトが役立つでしょう。

(編集)部分的な回答:境界-除去のチェック:

これは、次のHotSpot内部Wikiへの提供されたリンクから取得されます:https ://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination

HotSpotは、次の条件ですべてのforループの境界チェックを排除します。

  • 配列はループ不変です(ループ内で再割り当てされません)
  • インデックス変数は一定のストライドを持ちます(可能な場合は1つのスポットでのみ一定量だけ増加/減少します)
  • 配列は、変数の線形関数によってインデックスが付けられます。

例: int val = array[index*2 + 5]

また: int val = array[index+9]

いいえ: int val = array[Math.min(var,index)+7]


コードの初期バージョン:

これはサンプルバージョンです。これはH2データベースプロジェクトの未リリースバージョンのコードであるため、盗まないでください。最終バージョンはオープンソースになります。これは、ここのコードの最適化です:H2CompressLZFコード

論理的には、これは開発バージョンと同じですが、for(...)ループを使用して入力をステップスルーし、if/elseループを使用してリテラルモードと後方参照モードの間の異なるロジックを実行します。アレイへのアクセスを減らし、モード間のチェックを行います。

public int compressNewer(final byte[] in, final int inLen, final byte[] out, int outPos){
        int inPos = 0;
        // initialize the hash table
        if (cachedHashTable == null) {
            cachedHashTable = new int[HASH_SIZE];
        } else {
            System.arraycopy(EMPTY, 0, cachedHashTable, 0, HASH_SIZE);
        }
        int[] hashTab = cachedHashTable;
        // number of literals in current run
        int literals = 0;
        int future = first(in, inPos);
        final int endPos = inLen-4;

        // Loop through data until all of it has been compressed
        while (inPos < endPos) {
                future = (future << 8) | in[inPos+2] & 255;
//                hash = next(hash,in,inPos);
                int off = hash(future);
                // ref = possible index of matching group in data
                int ref = hashTab[off];
                hashTab[off] = inPos;
                off = inPos - ref - 1; //dropped for speed

                // has match if bytes at ref match bytes in future, etc
                // note: using ref++ rather than ref+1, ref+2, etc is about 15% faster
                boolean hasMatch = (ref > 0 && off <= MAX_OFF && (in[ref++] == (byte) (future >> 16) && in[ref++] == (byte)(future >> 8) && in[ref] == (byte)future));

                ref -=2; // ...EVEN when I have to recover it
                // write out literals, if max literals reached, OR has a match
                if ((hasMatch && literals != 0) || (literals == MAX_LITERAL)) {
                    out[outPos++] = (byte) (literals - 1);
                    System.arraycopy(in, inPos - literals, out, outPos, literals);
                    outPos += literals;
                    literals = 0;
                }

                //literal copying split because this improved performance by 5%

                if (hasMatch) { // grow match as much as possible
                    int maxLen = inLen - inPos - 2;
                    maxLen = maxLen > MAX_REF ? MAX_REF : maxLen;
                    int len = 3;
                    // grow match length as possible...
                    while (len < maxLen && in[ref + len] == in[inPos + len]) {
                        len++;
                    }
                    len -= 2;

                    // short matches write length to first byte, longer write to 2nd too
                    if (len < 7) {
                        out[outPos++] = (byte) ((off >> 8) + (len << 5));
                    } else {
                        out[outPos++] = (byte) ((off >> 8) + (7 << 5));
                        out[outPos++] = (byte) (len - 7);
                    }
                    out[outPos++] = (byte) off;
                    inPos += len;

                    //OPTIMIZATION: don't store hashtable entry for last byte of match and next byte
                    // rebuild neighborhood for hashing, but don't store location for this 3-byte group
                    // improves compress performance by ~10% or more, sacrificing ~2% compression...
                    future = ((in[inPos+1] & 255) << 16) | ((in[inPos + 2] & 255) << 8) | (in[inPos + 3] & 255);
                    inPos += 2;
                } else { //grow literals
                    literals++;
                    inPos++;
                } 
        }
        
        // write out remaining literals
        literals += inLen-inPos;
        inPos = inLen-literals;
        if(literals >= MAX_LITERAL){
            out[outPos++] = (byte)(MAX_LITERAL-1);
            System.arraycopy(in, inPos, out, outPos, MAX_LITERAL);
            outPos += MAX_LITERAL;
            inPos += MAX_LITERAL;
            literals -= MAX_LITERAL;
        }
        if (literals != 0) {
            out[outPos++] = (byte) (literals - 1);
            System.arraycopy(in, inPos, out, outPos, literals);
            outPos += literals;
        }
        return outPos; 
    }

最終編集:

締め切りが迫っているので、これまでのところ、ベストアンサーをマークしました。私はコードを投稿することを決定する前に非常に長い時間がかかったので、私は可能な限り賛成票を投じてコメントに返信し続けます。 コードが乱雑な場合はお詫びします。これは開発中のコードであり、コミットのために洗練されたものではありません。

4

4 に答える 4

19

完全な答えではありません。質問に必要な詳細なベンチマークを実行する時間がありませんが、役立つことを願っています。

己の敵を知れ

JVM(本質的にはJIT)と基盤となるCPU/メモリサブシステムの組み合わせをターゲットにしています。したがって、より積極的な最適化に移行する場合、「これはJVMXでより高速です」がすべての場合に有効であるとは限りません。

ターゲット市場/アプリケーションが主に特定のアーキテクチャで実行される場合は、それに固有のツールへの投資を検討する必要があります。* x86でのパフォーマンスが重要な要素である場合、インテルのVTuneは、説明する種類のjit出力分析にドリルダウンするのに最適です。* 64ビットと32ビットのJITの違いはかなりのものになる可能性があります。特に、呼び出し規約が変更される可能性があり、登録の機会が大きく異なるx86プラットフォームではそうです。

適切なツールを入手する

サンプリングプロファイラーを入手することをお勧めします。特定のニーズに対応するインストルメンテーションのオーバーヘッド(および、インライン化、キャッシュの汚染、コードサイズのインフレなどの関連するノックオン)は非常に大きくなります。Intel VTuneアナライザーは、他のアナライザーほど緊密ではありませんが、実際にはJavaで使用できます。
Sun JVMを使用していて、最新/最高のバージョンが何をしているのかを知っているだけで満足している場合、アセンブリを少し知っていれば、JITの出力を調査するために利用できるオプションはかなりあります。この記事では、この機能を使用したいくつかの興味深い分析について詳しく説明します

他の実装から学ぶ

変更履歴変更履歴は、以前のインラインアセンブリが実際には逆効果であり、コンパイラが出力を完全に制御できるようにすることで(アセンブリ内のディレクティブではなくコードを微調整して)、より良い結果が得られたことを示しています。

いくつかの詳細

LZFは、最新のデスクトップCPUでの効率的なアンマネージド実装では、メモリ帯域幅が大幅に制限されているため(したがって、最適化されていないmemcpyの速度と比較されます)、完全にレベル1キャッシュ内にとどまるようにコーディングする必要があります。
そのため、定数にできない静的フィールドは同じクラス内に配置する必要があります。これらの値は、クラスに関連付けられたvtableとメタデータ専用のメモリの同じ領域内に配置されることが多いためです。

エスケープ分析(1.6以降のみ)でトラップできないオブジェクトの割り当ては回避する必要があります。

cコードは、ループ展開を積極的に利用します。ただし、古い(1.4時代)VMでのこれのパフォーマンスは、JVMのモードに大きく依存します。明らかに、後者のsun jvmバージョンは、特にサーバーモードで、インライン化と展開に積極的です。

JITによって生成されたプリフェッチ命令は、メモリバウンドに近いこのようなコードにすべての違いをもたらす可能性があります。

「それは私たちのためにまっすぐに来ています」

あなたのターゲットは動いています、そして動き続けます。再びマーク・レーマンの以前の経験:

デフォルトのHLOGサイズは15になりました(CPUキャッシュが増加しました)

jvmのマイナーな更新でさえ、コンパイラの大幅な変更を伴う可能性があります

6544668実行時に整列できない配列操作をvecorizedしないでください。6536652いくつかのスーパーワード(SIMD)最適化を実装する6531696Intelcpusのメモリへの即時16ビット値ストアを使用しない6468290CPUごとにedenから分割して割り当てる

キャプテン明らか

測定、測定、測定。ライブラリに(別のdllに)関連情報(VMバージョン、CPU、OS、コマンドラインスイッチなど)をログに記録するシンプルで実行しやすいベンチマークを含めることができれば、これを簡単に返送できます。あなたのカバレッジを増やしてください、何よりもあなたがそれを気にかけているそれらの人々をカバーするでしょう。

于 2009-09-11T22:00:14.700 に答える
6

境界チェックの排除に関する限り、新しいJDKには、可能な場合はいつでも、それを排除する改善されたアルゴリズムがすでに含まれていると思います。これらは、この主題に関する2つの主要な論文です。

  • V. Mikheev、S。Fedoseev、V。Sukharev、N。Lipsky 2002Java でのループバージョニングの効果的な拡張リンク。このペーパーは、JetJVMにこの手法を実装したExcelsiorのスタッフからのものです
  • Würthinger、Thomas、Christian Wimmer、HanspeterMössenböck。2007.JavaHotSpotクライアントコンパイラの配列境界チェックの削除。PPPJ。リンク。上記の論文に少し基づいて、これは次のJDKに含まれると私が信じている実装です。達成されたスピードアップも提示されます。

このブログエントリもあります。このブログエントリでは、論文の1つについて表面的に説明し、配列だけでなく、新しいJDKの算術についてもベンチマーク結果を示しています。上記の論文の著者がいくつかの非常に興味深いコメントを提示し、議論を議論しているので、ブログエントリのコメントも非常に興味深いものです。また、この主題に関する他の同様のブログ投稿へのいくつかのポインタがあります。

それが役に立てば幸い。

于 2009-09-14T15:35:50.027 に答える
3

LZWのような単純な数値計算アルゴリズムを最適化することでJITコンパイラを大いに支援する必要がある可能性はかなり低いです。ShuggyCoUkはこれについて言及しましたが、私はそれが特別な注意に値すると思います:

コードのキャッシュフレンドリーは大きな要因になります。

ウォーキングセットのサイズを縮小し、データアクセスの局所性を可能な限り改善する必要があります。あなたは「パフォーマンスのためにバイトをintにパックする」と言っています。これは、intを使用してバイト値を保持し、それらをワード整列させるように聞こえます。そうしないでください!増加したデータセットサイズは、どんな利益よりも重要です(私は、ECC番号計算コードをint[]からbyte[]に変換し、2倍の速度を上げました)。

あなたがこれを知らないという偶然の機会に:あなたがいくつかのデータをバイトとintの両方として扱う必要があるなら、あなたはそれをシフトして|-マスクする必要はありません-ByteBuffer.asIntBuffer()そして関連するメソッドを使用してください。

現在の1.6JVMでは、System.arraycopyがコピーループを打ち負かす前に、いくつの要素をコピーする必要がありますか?

ベンチマークは自分で行う方がよいでしょう。私がJavaで1.3回行ったとき、それはおよそ2000要素でした。

于 2009-09-14T20:10:52.510 に答える
2

これまでのところ多くの答えがありますが、いくつかの追加事項があります。

  • 測定、測定、測定。ほとんどのJava開発者がマイクロベンチマークに対して警告しているのと同じように、変更間のパフォーマンスを比較するようにしてください。測定可能な改善をもたらさない最適化は、一般的に維持する価値がありません(もちろん、時にはそれは物事の組み合わせであり、それはよりトリッキーになります)
  • タイトなループは、JavaでもCと同じくらい重要です(変数の割り当てについても同様です。直接制御することはありませんが、HotSpotは最終的にそれを実行する必要があります)。シングルバイトのケース(7ビットASCII)を処理するためのタイトなループをタイトな(より)内部ループとして再配置し、他のケースを除外することで、UTF-8デコードの速度を実質的に2倍にすることができます。
  • 大きな配列の割り当てやクリアのコストを過小評価しないでください。lzfのエンコード/デコードを(メガバイトサイズだけでなく)小さな/中程度のチャンクでも高速にしたい場合は、byte [] /int[のすべての割り当てに注意してください。 ]ややコストがかかります。GCのせいではなく、JVMがスペースをクリアしなければならないからです。

H2の実装もかなり最適化されています(たとえば、ハッシュ配列がクリアされなくなります。これは多くの場合意味があります)。そして私は実際に別のJavaプロジェクトで使用するためにそれを変更するのを手伝いました。私の貢献は主に、ストリーミング以外の場合に最適になるように変更することでしたが、それはタイトなエンコード/デコードループには影響しませんでした。

于 2009-12-08T05:38:50.203 に答える