16

Java および C# 用の 32 コア サーバーで同じ関数を実行する多くのスレッドの生成をテストしています。スレッドプールを使用して、1、2、4、8、16、または 32 個のスレッドにわたってバッチ処理される関数の 1000 回の反復でアプリケーションを実行します。

1、2、4、8、および 16 の同時スレッドで、Java は少なくとも C# の 2 倍高速です。ただし、スレッド数が増えるとギャップが縮まり、32 スレッドまでに C# の平均実行時間はほぼ同じになりますが、Java は 2000 ミリ秒かかることがあります (一方、両方の言語は通常約 400 ミリ秒実行されます)。Java は、スレッドの反復ごとにかかる時間の大幅なスパイクにより、悪化し始めています。

編集これはWindows Server 2008です

EDIT2 以下のコードを変更して、Executor Service スレッドプールの使用を示すようにしました。Java 7もインストールしました。

ホットスポット VM で次の最適化を設定しました。

-XX:+UseConcMarkSweepGC -Xmx 6000

しかし、それはまだ物事を改善していません。コードの唯一の違いは、以下のスレッドプールを使用していることと、使用する C# バージョンであることです。

http://www.codeproject.com/Articles/7933/Smart-Thread-Pool

Java をより最適化する方法はありますか? パフォーマンスが大幅に低下している理由を説明できますか?

より効率的な Java スレッドプールはありますか?

(テスト関数を変更するという意味ではないことに注意してください)

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class PoolDemo {

    static long FastestMemory = 2000000;
    static long SlowestMemory = 0;
    static long TotalTime;
    static int[] FileArray;
    static DataOutputStream outs;
    static FileOutputStream fout;
    static Byte myByte = 0;

  public static void main(String[] args) throws InterruptedException, FileNotFoundException {

        int Iterations = Integer.parseInt(args[0]);
        int ThreadSize = Integer.parseInt(args[1]);

        FileArray = new int[Iterations];
        fout = new FileOutputStream("server_testing.csv");

        // fixed pool, unlimited queue
        ExecutorService service = Executors.newFixedThreadPool(ThreadSize);
        ThreadPoolExecutor executor = (ThreadPoolExecutor) service;

        for(int i = 0; i<Iterations; i++) {
          Task t = new Task(i);
          executor.execute(t);
        }

        for(int j=0; j<FileArray.length; j++){
            new PrintStream(fout).println(FileArray[j] + ",");
        }
      }

  private static class Task implements Runnable {

    private int ID;

    public Task(int index) {
      this.ID = index;
    }

    public void run() {
        long Start = System.currentTimeMillis();

        int Size1 = 100000;
        int Size2 = 2 * Size1;
        int Size3 = Size1;

        byte[] list1 = new byte[Size1];
        byte[] list2 = new byte[Size2];
        byte[] list3 = new byte[Size3];

        for(int i=0; i<Size1; i++){
            list1[i] = myByte;
        }

        for (int i = 0; i < Size2; i=i+2)
        {
            list2[i] = myByte;
        }

        for (int i = 0; i < Size3; i++)
        {
            byte temp = list1[i];
            byte temp2 = list2[i];
            list3[i] = temp;
            list2[i] = temp;
            list1[i] = temp2;
        }

        long Finish = System.currentTimeMillis();
        long Duration = Finish - Start;
        TotalTime += Duration;
        FileArray[this.ID] = (int)Duration;
        System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");


        if(Duration < FastestMemory){
            FastestMemory = Duration;
        }
        if (Duration > SlowestMemory)
        {
            SlowestMemory = Duration;
        }
    }
  }
}
4

4 に答える 4

19

概要

以下は、元の応答、更新 1、および更新 2 です。更新 1 では、同時実行構造を使用してテスト統計変数の競合状態に対処することについて説明しています。Update 2 は、競合状態の問題に対処するためのはるかに簡単な方法です。私からの更新がこれ以上ないことを願っています - 応答が長くなって申し訳ありませんが、マルチスレッド プログラミングは複雑です!

元の応答

コードの唯一の違いは、以下のスレッドプールを使用していることです

それは絶対に大きな違いだと言えます。2 つの言語のスレッド プールの実装が、ユーザー空間で記述された完全に異なるコード ブロックである場合、2 つの言語のパフォーマンスを比較することは困難です。スレッド プールの実装は、パフォーマンスに大きな影響を与える可能性があります。

Java 独自の組み込みスレッド プールの使用を検討する必要があります。ThreadPoolExecutorと、それが含まれるjava.util.concurrentパッケージ全体を参照してください。Executorsクラスには、プール用の便利な静的ファクトリ メソッドがあり、優れた上位レベルのインターフェイスです。必要なのは JDK 1.5+ だけですが、新しいほど優れています。他のポスターで言及されている fork/join ソリューションもこのパッケージの一部です - 言及されているように、それらには 1.7+ が必要です。

更新 1 - 同時実行構造を使用して競合状態に対処する

FastestMemory、、SlowestMemoryおよびの設定で競合状態が発生していますTotalTime。最初の 2 つの場合は、複数の手順でテストと設定を<行っています。>これはアトミックではありません。テストと設定の間に別のスレッドがこれらの値を更新する可能性は確かにあります。の+=設定TotalTimeもアトミックではありません。つまり、変装したテストと設定です。

ここにいくつかの推奨される修正があります。

合計時間

ここでの目標は、 のスレッドセーフなアトミック+=ですTotalTime

// At the top of everything
import java.util.concurrent.atomic.AtomicLong;  

...    

// In PoolDemo
static AtomicLong TotalTime = new AtomicLong();    

...    

// In Task, where you currently do the TotalTime += piece
TotalTime.addAndGet (Duration); 

最も速いメモリ/最も遅いメモリ

ここでの目標は、テストと更新FastestMemorySlowestMemoryそれぞれアトミック ステップで行うことです。そのため、スレッドがテスト ステップと更新ステップの間に入り込んで競合状態が発生することはありません。

最も簡単なアプローチ:

クラス自体をモニターとして使用して、変数のテストと設定を保護します。同期された可視性を保証するために、変数を含むモニターが必要です (これをキャッチしてくれた @AH に感謝します)。クラス自体を使用する必要がありstaticます。

// In Task
synchronized (PoolDemo.class) {
    if (Duration < FastestMemory) {
        FastestMemory = Duration;
    }

    if (Duration > SlowestMemory) {
        SlowestMemory = Duration;
    }
}

中間アプローチ:

モニターにクラス全体を使用したり、クラスを使用してモニターを公開したりするのは好きではないかもしれません。FastestMemoryandSlowestMemoryを含まない別のモニターを作成することもできますが、同期の可視性の問題が発生します。volatileキーワードを使用してこれを回避します。

// In PoolDemo
static Integer _monitor = new Integer(1);
static volatile long FastestMemory = 2000000;
static volatile long SlowestMemory = 0;

...

// In Task
synchronized (PoolDemo._monitor) {
    if (Duration < FastestMemory) {
        FastestMemory = Duration;
    }

    if (Duration > SlowestMemory) {
        SlowestMemory = Duration;
    }
}

高度なアプローチ:

ここではjava.util.concurrent.atomic、モニターの代わりにクラスを使用します。競合が激しい場合、これはsynchronizedアプローチよりも優れたパフォーマンスを発揮するはずです。試してみてください。

// At the top of everything
import java.util.concurrent.atomic.AtomicLong;    

. . . . 

// In PoolDemo
static AtomicLong FastestMemory = new AtomicLong(2000000);
static AtomicLong SlowestMemory = new AtomicLong(0);

. . . . .

// In Task
long temp = FastestMemory.get();       
while (Duration < temp) {
    if (!FastestMemory.compareAndSet (temp, Duration)) {
        temp = FastestMemory.get();       
    }
}

temp = SlowestMemory.get();
while (Duration > temp) {
    if (!SlowestMemory.compareAndSet (temp, Duration)) {
        temp = SlowestMemory.get();
    }
}

この後どうなるか教えてください。問題は解決しないかもしれませんが、パフォーマンスを追跡する変数そのものの競合状態は無視できないほど危険です。

最初はこの更新をコメントとして投稿しましたが、コードを表示するスペースを確保するためにここに移動しました。この更新は数回繰り返されました -以前のバージョンで私が持っていたバグを見つけてくれたAHに感謝します。この更新の内容は、コメントの内容に優先します。

最後になりましたが、このすべての資料をカバーする優れた情報源はJava Concurrency in Practiceです。これは、Java の同時実行に関する最高の本であり、全体として最高の Java 本の 1 つです。

更新 2 - はるかに簡単な方法で競合状態に対処する

を追加しない限り、現在のコードは決して終了しないことに最近気付きましたexecutorService.shutdown()。つまり、そのプールに存在する非デーモン スレッドを終了する必要があります。終了しないと、メイン スレッドが終了しません。これにより、すべてのスレッドが終了するのを待たなければならないので、終了後にそれらの期間を比較してFastestMemory、などの同時更新を完全にバイパスしてみませんか? これはより簡単で、より高速になる可能性があります。ロックや CAS のオーバーヘッドはもうありませFileArrayん。とにかく、物事の最後にすでに反復を行っています。

もう 1 つの利点は、各スレッドが個別のセルに書き込みを行っており、書き込み中に を読み取ることがないため、 の同時更新FileArrayが完全に安全でFileArrayあることです。

これにより、次の変更を行います。

// In PoolDemo
// This part is the same, just so you know where we are
for(int i = 0; i<Iterations; i++) {
    Task t = new Task(i);
    executor.execute(t);
}

// CHANGES BEGIN HERE
// Will block till all tasks finish. Required regardless.
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);

for(int j=0; j<FileArray.length; j++){
    long duration = FileArray[j];
    TotalTime += duration;

    if (duration < FastestMemory) {
        FastestMemory = duration;
    }

    if (duration > SlowestMemory) {
        SlowestMemory = duration;
    }

    new PrintStream(fout).println(FileArray[j] + ",");
}

. . . 

// In Task
// Ending of Task.run() now looks like this
long Finish = System.currentTimeMillis();
long Duration = Finish - Start;
FileArray[this.ID] = (int)Duration;
System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

このアプローチも試してみてください。

同様の競合状態について、C# コードを確実にチェックする必要があります。

于 2012-04-04T12:41:40.617 に答える
5

...しかし、Java は時々 2000ms かかります...

    byte[] list1 = new byte[Size1];
    byte[] list2 = new byte[Size2];
    byte[] list3 = new byte[Size3];

ヒックアップは、配列をクリーンアップするガベージ コレクターになります。本当に調整したい場合は、配列に何らかのキャッシュを使用することをお勧めします。

編集

これです

   System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

synchronized内部で 1 つ以上を実行します。したがって、高度な「同時実行」コードは、この時点で非常に適切にシリアル化されます。削除して再テストするだけです。

于 2012-04-08T12:43:13.400 に答える
4

@sparc_spreadの答えは素晴らしいですが、私が気づいたもう1つのことは次のとおりです。

関数を 1000 回繰り返してアプリケーションを実行します

HotSpot JVM は、クライアント モードでは任意の関数の最初の 1.5k 反復、サーバー モードでは 10k 反復でインタープリターモードで動作していることに注意してください。多くのコアを持つコンピューターは、HotSpot JVM によって自動的に「サーバー」と見なされます。

つまり、C# は Java よりも前に JIT を実行 (およびマシン コードで実行) し、関数の実行時のパフォーマンスが向上する可能性があることを意味します。反復を 20,000 に増やし、10k 反復からカウントを開始してみてください。

ここでの理論的根拠は、JVM が JIT を最適に実行する方法について統計データを収集することです。関数が長時間実行されることを信頼しているため、実行時間を全体的に高速化するには、「低速ブートストラップ」メカニズムが必要です。または、「関数の 20% が 80% の時間実行される」という彼らの言葉では、なぜそれらすべてを JIT するのでしょうか?

于 2012-04-08T04:48:27.117 に答える
2

Are you using java6? Java 7 comes with features to improve performance in parallel programing:

http://www.oracle.com/technetwork/articles/java/fork-join-422606.html

于 2012-04-04T12:26:19.310 に答える