32

私は、ダイレクト バイト バッファで最適に機能するいくつかの -to- コードに取り組んでいます - 長寿命で大きい (1 接続あたり数十から数百メガバイト) SocketChannel。対パフォーマンスのベンチマーク。SocketChannelFileChannelByteBuffer.allocate()ByteBuffer.allocateDirect()

その結果には、うまく説明できない驚きがありました。以下のグラフでは、256KB と 512KB でByteBuffer.allocate()転送実装の非常に顕著な崖があり、パフォーマンスは最大 50% 低下しています。のパフォーマンスの崖も小さいようですByteBuffer.allocateDirect()。(%-gain シリーズは、これらの変化を視覚化するのに役立ちます。)

バッファ サイズ (バイト) と時間 (MS)

ポニーギャップ

ByteBuffer.allocate()との間の奇妙な性能曲線の差はなぜByteBuffer.allocateDirect()ですか? カーテンの後ろで何が起こっているのですか?

ハードウェアとOSに依存する可能性が非常に高いため、詳細は次のとおりです。

  • デュアルコア Core 2 CPU を搭載した MacBook Pro
  • インテル X25M SSD ドライブ
  • OS X 10.6.4

ソースコード、ご要望に応じて:

package ch.dietpizza.bench;

import static java.lang.String.format;
import static java.lang.System.out;
import static java.nio.ByteBuffer.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

public class SocketChannelByteBufferExample {
    private static WritableByteChannel target;
    private static ReadableByteChannel source;
    private static ByteBuffer          buffer;

    public static void main(String[] args) throws IOException, InterruptedException {
        long timeDirect;
        long normal;
        out.println("start");

        for (int i = 512; i <= 1024 * 1024 * 64; i *= 2) {
            buffer = allocateDirect(i);
            timeDirect = copyShortest();

            buffer = allocate(i);
            normal = copyShortest();

            out.println(format("%d, %d, %d", i, normal, timeDirect));
        }

        out.println("stop");
    }

    private static long copyShortest() throws IOException, InterruptedException {
        int result = 0;
        for (int i = 0; i < 100; i++) {
            int single = copyOnce();
            result = (i == 0) ? single : Math.min(result, single);
        }
        return result;
    }


    private static int copyOnce() throws IOException, InterruptedException {
        initialize();

        long start = System.currentTimeMillis();

        while (source.read(buffer)!= -1) {    
            buffer.flip();  
            target.write(buffer);
            buffer.clear();  //pos = 0, limit = capacity
        }

        long time = System.currentTimeMillis() - start;

        rest();

        return (int)time;
    }   


    private static void initialize() throws UnknownHostException, IOException {
        InputStream  is = new FileInputStream(new File("/Users/stu/temp/robyn.in"));//315 MB file
        OutputStream os = new FileOutputStream(new File("/dev/null"));

        target = Channels.newChannel(os);
        source = Channels.newChannel(is);
    }

    private static void rest() throws InterruptedException {
        System.gc();
        Thread.sleep(200);      
    }
}
4

4 に答える 4

30

ByteBuffer がどのように機能するか、および Direct (Byte)Buffers が現在唯一真に有用である理由。

最初は、それが一般的な知識ではないことに少し驚いていますが、私と一緒に我慢してください

ダイレクト バイト バッファーは、Java ヒープの外部にアドレスを割り当てます。

これは非常に重要です。すべての OS (およびネイティブ C) 関数は、ヒープ上のオブジェクトをロックしてデータをコピーすることなく、そのアドレスを利用できます。コピーの簡単な例: Socket.getOutputStream().write(byte[]) 経由でデータを送信するには、ネイティブ コードで byte[] を「ロック」し、それを Java ヒープの外にコピーしてから、OS 関数を呼び出す必要があります。送信します。コピーは、スタック (小さい byte[] の場合) で実行されるか、大きいものでは malloc/free を介して実行されます。DatagramSockets も例外ではなく、コピーも行います。ただし、64KB に制限され、スレッド スタックが十分に大きくない場合や再帰が深くない場合にプロセスを強制終了する可能性があるスタックに割り当てられます。 注: ロックすると、JVM/GC がヒープ内でオブジェクトを移動/再割り当てできなくなります

そのため、NIO の導入により、コピーと多数のストリーム パイプライン/インダイレクションを回避するというアイデアが生まれました。多くの場合、データが宛先に到達する前に、3 ~ 4 種類のバッファリングされたストリームがあります。(イェーイ ポーランドは美しいショットでイコライズ (!) します) ダイレクト バッファを導入することで、Java はロックやコピーを必要とせずに C ネイティブ コードと直接通信できます。したがってsent、関数はバッファのアドレスに位置を追加することができ、パフォーマンスはネイティブ C とほぼ同じです。それはダイレクト バッファに関するものです。

ダイレクト バッファに関する主な問題 - それらは、byte[] とは異なり、割り当てと解放にコストがかかり、使用するのが非常に面倒です。

非ダイレクト バッファは、ダイレクト バッファが提供する真の本質を提供しません。つまり、ネイティブ/OS へのダイレクト ブリッジではなく、軽量でまったく同じ API を共有しwrap byte[]ます。直接操作 - 愛してはいけないことは何ですか? まあ、それらはコピーする必要があります!

では、OS /ネイティブが使用できないため、Sun / Oracleは非直接バッファをどのように処理しますか-まあ、素朴に。非ダイレクト バッファを使用する場合、ダイレクト カウンタ部分を作成する必要があります。この実装は、作成の多額のコストを回避するために、 * をThreadLocal介していくつかの直接バッファーを使用およびキャッシュするのに十分スマートです。単純な部分は、それらをコピーするときに発生します。毎回SoftReferenceバッファー全体 ( ) をコピーしようとします。remaining()

ここで想像してみてください: 512 KB の非直接バッファーが 64 KB のソケット バッファーに移動すると、ソケット バッファーはそのサイズを超えることはありません。したがって、最初は 512 KB が非直接からスレッドローカル直接にコピーされますが、そのうち 64 KB のみが使用されます。次回は 512-64 KB がコピーされますが 64 KB しか使用されず、3 回目は 512-64*2 KB がコピーされますが 64 KB だけが使用されます。バッファは完全に空になります。nしたがって、合計で KB をコピーするだけでなく、 n× n÷ m( n= 512, m= 16 (ソケット バッファーが残した平均スペース)) をコピーすることになります。

コピー部分は、すべての非直接バッファーへの共通/抽象パスであるため、実装はターゲット容量を認識しません。コピーすると、キャッシュが破棄され、メモリ帯域幅が減少します。

* SoftReference キャッシングに関する注意: GC の実装に依存し、エクスペリエンスは異なる場合があります。Sun の GC は、空きヒープ メモリを使用して SoftRefences の寿命を決定します。これにより、SoftRefences が解放されたときに厄介な動作が発生します。アプリケーションは、以前にキャッシュされたオブジェクトを再度割り当てる必要があります。少なくとも、余分なキャッシュの破棄には影響しませんが、代わりに影響を受けます)

私の経験則 - ソケットの読み取り/書き込みバッファーのサイズのプールされた直接バッファー。OS が必要以上にコピーすることはありません。

このマイクロベンチマークは主にメモリ スループット テストであり、OS はファイルを完全にキャッシュに格納するため、主にテストしmemcpyます。バッファーが L2 キャッシュを使い果たすと、パフォーマンスの低下が顕著になります。また、そのようなベンチマークを実行すると、GC コレクションのコストが増加し、蓄積されます。(rest()ソフト参照された ByteBuffers を収集しません)

于 2012-06-12T20:20:44.690 に答える
26

スレッド ローカル割り当てバッファー (TLAB)

テスト中のスレッド ローカル アロケーション バッファ (TLAB) は 256K 程度でしょうか。TLAB を使用すると、ヒープからの割り当てが最適化されるため、<=256K の非直接割り当てが高速になります。

一般的に行われているのは、各スレッドに、そのスレッドが割り当てを行うために排他的に使用するバッファーを与えることです。ヒープからバッファを割り当てるには同期を使用する必要がありますが、その後、スレッドは同期なしでバッファから割り当てることができます。ホットスポット JVM では、これらをスレッド ローカル割り当てバッファー (TLAB) と呼びます。彼らはうまく機能します。

TLAB をバイパスする大きな割り当て

256K TLAB に関する私の仮説が正しければ、この記事の後半にある情報は、より大きな非直接バッファーの 256K を超える割り当てが TLAB をバイパスする可能性があることを示唆しています。これらの割り当てはヒープに直接行われ、スレッドの同期が必要になるため、パフォーマンスが低下します。

TLAB から作成できない割り当ては、スレッドが新しい TLAB を取得する必要があることを常に意味するわけではありません。割り当てのサイズと TLAB に残っている未使用スペースに応じて、VM はヒープからのみ割り当てを行うことを決定できます。ヒープからの割り当てには同期が必要ですが、新しい TLAB を取得する必要もあります。割り当てが大きい (現在の TLAB サイズのかなりの部分) と見なされた場合、割り当ては常にヒープから行われます。これにより無駄が削減され、平均よりもはるかに大きな割り当てが適切に処理されました。

TLAB パラメータの微調整

この仮説は、TLAB を微調整して診断情報を取得する方法を示す後の記事の情報を使用してテストできます。

特定の TLAB サイズを試すには、2 つの -XX フラグを設定する必要があります。1 つは初期サイズを定義し、もう 1 つはサイズ変更を無効にします。

-XX:TLABSize= -XX:-ResizeTLAB

tlab の最小サイズは -XX:MinTLABSize で設定され、デフォルトは 2K バイトです。最大サイズは、整数 Java 配列の最大サイズであり、GC スカベンジが発生したときに TLAB の未割り当て部分を埋めるために使用されます。

診断印刷オプション

-XX:+PrintTLAB

各スキャベンジで、スレッドごとに 1 行 (「TLAB: gc thread:」で始まり、「」は除く) と要約行を 1 行出力します。

于 2010-09-06T17:35:46.707 に答える
7

これらのひざは、CPU キャッシュ境界を越えてつまずいたことが原因であると思われます。「非直接」バッファ read()/write() 実装は、「直接」バッファ read()/write() 実装と比較して追加のメモリ バッファ コピーが原因で、以前に「キャッシュ ミス」します。

于 2010-10-01T21:44:47.650 に答える
0

これが起こる理由はたくさんあります。コードやデータに関する詳細がなければ、何が起こっているのかを推測することしかできません。

いくつかの推測:

  • 一度に読み取ることができる最大バイト数に達した可能性があるため、IOwaits が高くなったり、ループが減少することなくメモリ消費量が増加したりします。
  • 重大なメモリ制限に達したか、JVM が新しい割り当ての前にメモリを解放しようとしている可能性があります。-Xmxパラメータと-Xmsパラメータをいじってみてください
  • 一部のメソッドの呼び出し数が少なすぎるため、HotSpot が最適化できない/最適化しない可能性があります。
  • この種の遅延を引き起こすOSまたはハードウェアの状態がある可能性があります
  • たぶん、JVM の実装にはバグがあります ;-)
于 2010-09-06T13:51:55.350 に答える