37

私のアプリケーションでは、数十から数百のアクションを並行して実行します(アクションの戻り値はありません)。

どのアプローチが最適か:

  1. 配列Task.Factory.StartNewを反復処理するforeachループでの使用( )ActionAction[]

    Task.Factory.StartNew(() => someAction());

  2. 配列であるParallelクラスの使用( )actionsActionAction[]

    Parallel.Invoke(actions);

これらの2つのアプローチは同等ですか?パフォーマンスへの影響はありますか?

編集

私はいくつかのパフォーマンステストを実行しましたが、私のマシン(それぞれ2 CPU 2コア)での結果は非常に似ているようです。1CPUのような他のマシンでどのように見えるかわかりません。また、メモリ消費量が何であるかわかりません(非常に正確な方法でテストする方法がわかりません)。

4

3 に答える 3

47

これら2つの最も重要な違いはParallel.Invoke、コードを続行する前にすべてのアクションが完了するのを待つのに対し、コードStartNewの次の行に移動して、タスクが適切な時間に完了することを可能にすることです。

このセマンティックの違いは、最初の(そしておそらく唯一の)考慮事項です。しかし、情報提供の目的で、ここにベンチマークがあります:

/* This is a benchmarking template I use in LINQPad when I want to do a
 * quick performance test. Just give it a couple of actions to test and
 * it will give you a pretty good idea of how long they take compared
 * to one another. It's not perfect: You can expect a 3% error margin
 * under ideal circumstances. But if you're not going to improve
 * performance by more than 3%, you probably don't care anyway.*/
void Main()
{
    // Enter setup code here
    var actions2 =
    (from i in Enumerable.Range(1, 10000)
    select (Action)(() => {})).ToArray();

    var awaitList = new Task[actions2.Length];
    var actions = new[]
    {
        new TimedAction("Task.Factory.StartNew", () =>
        {
            // Enter code to test here
            int j = 0;
            foreach(var action in actions2)
            {
                awaitList[j++] = Task.Factory.StartNew(action);
            }
            Task.WaitAll(awaitList);
        }),
        new TimedAction("Parallel.Invoke", () =>
        {
            // Enter code to test here
            Parallel.Invoke(actions2);
        }),
    };
    const int TimesToRun = 100; // Tweak this as necessary
    TimeActions(TimesToRun, actions);
}


#region timer helper methods
// Define other methods and classes here
public void TimeActions(int iterations, params TimedAction[] actions)
{
    Stopwatch s = new Stopwatch();
    int length = actions.Length;
    var results = new ActionResult[actions.Length];
    // Perform the actions in their initial order.
    for(int i = 0; i < length; i++)
    {
        var action = actions[i];
        var result = results[i] = new ActionResult{Message = action.Message};
        // Do a dry run to get things ramped up/cached
        result.DryRun1 = s.Time(action.Action, 10);
        result.FullRun1 = s.Time(action.Action, iterations);
    }
    // Perform the actions in reverse order.
    for(int i = length - 1; i >= 0; i--)
    {
        var action = actions[i];
        var result = results[i];
        // Do a dry run to get things ramped up/cached
        result.DryRun2 = s.Time(action.Action, 10);
        result.FullRun2 = s.Time(action.Action, iterations);
    }
    results.Dump();
}

public class ActionResult
{
    public string Message {get;set;}
    public double DryRun1 {get;set;}
    public double DryRun2 {get;set;}
    public double FullRun1 {get;set;}
    public double FullRun2 {get;set;}
}

public class TimedAction
{
    public TimedAction(string message, Action action)
    {
        Message = message;
        Action = action;
    }
    public string Message {get;private set;}
    public Action Action {get;private set;}
}

public static class StopwatchExtensions
{
    public static double Time(this Stopwatch sw, Action action, int iterations)
    {
        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();

        return sw.Elapsed.TotalMilliseconds;
    }
}
#endregion

結果:

Message               | DryRun1 | DryRun2 | FullRun1 | FullRun2
----------------------------------------------------------------
Task.Factory.StartNew | 43.0592 | 50.847  | 452.2637 | 463.2310
Parallel.Invoke       | 10.5717 |  9.948  | 102.7767 | 101.1158 

ご覧のとおり、Parallel.Invokeを使用すると、多数の新しいタスクが完了するのを待つよりも約4.5倍速くなります。もちろん、それはあなたの行動が全く何もしないときです。各アクションが多ければ多いほど、気付く違いは少なくなります。

于 2013-01-02T23:40:36.780 に答える
14

物事の壮大なスキームでは、どのような場合でも実際に多くのタスクを処理するオーバーヘッドを考慮すると、2つの方法のパフォーマンスの違いはごくわずかです。

Parallel.Invoke基本的にあなたのために実行しTask.Factory.StartNew()ます。ですから、ここでは読みやすさがより重要だと思います。

また、StriplingWarriorが言及しているように、Parallel.InvokeWaitAll(すべてのタスクが完了するまでコードをブロックする)を実行するので、それを行う必要もありません。タスクが完了したときに気にせずにバックグラウンドでタスクを実行したい場合は、が必要ですTask.Factory.StartNew()

于 2013-01-02T23:36:01.013 に答える
13

StriplingWarrorのテストを使用して、違いがどこから来ているのかを調べました。これを行ったのは、Reflectorを使用してコードを確認すると、Parallelクラスは一連のタスクを作成して実行するのと何ら変わりはないからです。

理論的な観点からは、両方のアプローチは実行時間の点で同等である必要があります。しかし、空のアクションを使用した(あまり現実的ではない)テストでは、Parallelクラスの方がはるかに高速であることが示されました。

タスクバージョンは、ほとんどすべての時間を新しいタスクの作成に費やしているため、多くのガベージコレクションが発生します。表示される速度の違いは、純粋に、すぐにゴミになる多くのタスクを作成するという事実によるものです。

Parallelクラスは、代わりに、すべてのCPUで同時に実行される独自のタスク派生クラスを作成します。すべてのコアで実行されている物理タスクは1つだけです。同期はタスクデリゲート内で行われるようになりました。これは、Parallelクラスのはるかに高速な速度を説明しています。

ParallelForReplicatingTask task2 = new ParallelForReplicatingTask(parallelOptions, delegate {
        for (int k = Interlocked.Increment(ref actionIndex); k <= actionsCopy.Length; k = Interlocked.Increment(ref actionIndex))
        {
            actionsCopy[k - 1]();
        }
    }, TaskCreationOptions.None, InternalTaskOptions.SelfReplicating);
task2.RunSynchronously(parallelOptions.EffectiveTaskScheduler);
task2.Wait();

では、何が良いのでしょうか?最良のタスクは、決して実行されないタスクです。ガベージコレクターの負担になるほど多くのタスクを作成する必要がある場合は、タスクAPIから離れて、新しいタスクなしですべてのコアで直接並列実行できるParallelクラスを使用する必要があります。

さらに高速にする必要がある場合は、手動でスレッドを作成し、手動で最適化されたデータ構造を使用してアクセスパターンの速度を最大化することが、最もパフォーマンスの高いソリューションである可能性があります。ただし、TPLとParallel APIはすでに大幅に調整されているため、成功する可能性はほとんどありません。通常、実行中のタスクを構成するために多くのオーバーロードの1つを使用するか、はるかに少ないコードで同じことを実現するためにParallelクラスを使用する必要があります。

ただし、非標準のスレッドパターンがある場合は、コアを最大限に活用するためにTPLを使用しない方がよい場合があります。Stephen Toubでさえ、TPL APIは超高速パフォーマンス用に設計されていないことを述べましたが、主な目標は「平均的な」プログラマーがスレッド化を容易にすることでした。特定の場合にTPLを打ち負かすには、平均をはるかに上回っている必要があり、CPUキャッシュライン、スレッドスケジューリング、メモリモデル、JITコード生成など、特定のシナリオで何かを思い付くために多くのことを知る必要があります。より良い。

于 2013-01-03T14:19:02.597 に答える