7

基本的に double を表し、ロックを使用して安全な読み取りと書き込みを保証するスレッド セーフ オブジェクトを作成しています。これらのオブジェクトの多く (20 ~ 30 個) を 1 秒間に 100 回読み書きするコードで使用し、これらの各時間ステップの平均計算時間を測定しています。ゲッターの実装についていくつかのオプションを検討し始めました。多くのテストを実行し、計算時間の測定値を平均化するために多くのサンプルを収集した後、特定の実装が他の実装よりも一貫して優れていることがわかりましたが、期待する実装ではありませんでした。

実装 1) 平均計算時間 = 0.607ms:

protected override double GetValue()
{
    lock(_sync)
    {
        return _value;
    }
}

実装 2) 平均計算時間 = 0.615ms:

protected override double GetValue()
{
    double result;
    lock(_sync)
    {
        result = _value;
    }
    return result;
}

実装 3) 平均計算時間 = 0.560ms:

protected override double GetValue()
{
    double result = 0;
    lock(_sync)
    {
        result = _value;
    }
    return result;
}

私が期待したこと:実装 3 が 3 つの中で最悪であると予想していました (これは実際には私のオリジナルのコードであり、このように記述したのは偶然または怠惰なコーディングでした)。パフォーマンスの。実装 1 が最速であると予想します。とにかく上書きされる二重の結果への割り当てを削除しているだけなので、実装 2 は実装 3 よりも高速ではないにしても、少なくとも同じくらい高速であると予想していました。

私の質問は:これら 3 つの実装が私が測定した相対的なパフォーマンスを持っている理由を誰か説明できますか? 私には直感に反しているように思えますが、その理由を本当に知りたいです。

これらの違いは大きなものではないことはわかっていますが、テストを実行するたびに相対的な測定値が一貫しており、各テストで数千のサンプルを収集して計算時間を平均化しています。また、これらのテストを行っているのは、私のアプリケーションが非常に高いパフォーマンス、または少なくとも合理的に得られるパフォーマンスを必要とするためであることを覚えておいてください。私のテスト ケースは小さなテスト ケースであり、リリースで実行する場合、コードのパフォーマンスが重要になります。

編集: 私は MonoTouch を使用し、iPad Mini デバイスでコードを実行していることに注意してください。そのため、おそらく c# とは関係なく、MonoTouch のクロス コンパイラに関連するものです。

4

2 に答える 2

15

率直に言って、ここには他のより良いアプローチがあります。次の出力 (JIT 用の x1 は無視):

x5000000
Example1        128ms
Example2        136ms
Example3        129ms
CompareExchange 53ms
ReadUnsafe      54ms
UntypedBox      23ms
TypedBox        12ms

x5000000
Example1        129ms
Example2        129ms
Example3        129ms
CompareExchange 52ms
ReadUnsafe      53ms
UntypedBox      23ms
TypedBox        12ms

x5000000
Example1        129ms
Example2        161ms
Example3        129ms
CompareExchange 52ms
ReadUnsafe      53ms
UntypedBox      23ms
TypedBox        12ms

これらはすべてスレッドセーフな実装です。ご覧のとおり、型付きボックスが最も速く、型なし ( object) ボックスが続きます。次に来る (ほぼ同じ速度で) Interlocked.CompareExchange/ Interlocked.Read- 後者は のみをサポートすることに注意してlongくださいdouble

もちろん、ターゲット フレームワークでテストしてください。

楽しみのために、私もMutex;をテストしました。同じ規模のテストでは、約 3300 ミリ秒かかります。

using System;
using System.Diagnostics;
using System.Threading;
abstract class Experiment
{
    public abstract double GetValue();
}
class Example1 : Experiment
{
    private readonly object _sync = new object();
    private double _value = 3;
    public override double GetValue()
    {
        lock (_sync)
        {
            return _value;
        }
    }
}
class Example2 : Experiment
{
    private readonly object _sync = new object();
    private double _value = 3;
    public override double GetValue()
    {
        lock (_sync)
        {
            return _value;
        }
    }
}

class Example3 : Experiment
{
    private readonly object _sync = new object();
    private double _value = 3;
    public override double GetValue()
    {
        double result = 0;
        lock (_sync)
        {
            result = _value;
        }
        return result;
    }
}

class CompareExchange : Experiment
{
    private double _value = 3;
    public override double GetValue()
    {
        return Interlocked.CompareExchange(ref _value, 0, 0);
    }
}
class ReadUnsafe : Experiment
{
    private long _value = DoubleToInt64(3);
    static unsafe long DoubleToInt64(double val)
    {   // I'm mainly including this for the field initializer
        // in real use this would be manually inlined
        return *(long*)(&val);
    }
    public override unsafe double GetValue()
    {
        long val = Interlocked.Read(ref _value);
        return *(double*)(&val);
    }
}
class UntypedBox : Experiment
{
    // references are always atomic
    private volatile object _value = 3.0;
    public override double GetValue()
    {
        return (double)_value;
    }
}
class TypedBox : Experiment
{
    private sealed class Box
    {
        public readonly double Value;
        public Box(double value) { Value = value; }

    }
    // references are always atomic
    private volatile Box _value = new Box(3);
    public override double GetValue()
    {
        return _value.Value;
    }
}
static class Program
{
    static void Main()
    {
        // once for JIT
        RunExperiments(1);
        // three times for real
        RunExperiments(5000000);
        RunExperiments(5000000);
        RunExperiments(5000000);
    }
    static void RunExperiments(int loop)
    {
        Console.WriteLine("x{0}", loop);
        RunExperiment(new Example1(), loop);
        RunExperiment(new Example2(), loop);
        RunExperiment(new Example3(), loop);
        RunExperiment(new CompareExchange(), loop);
        RunExperiment(new ReadUnsafe(), loop);
        RunExperiment(new UntypedBox(), loop);
        RunExperiment(new TypedBox(), loop);
        Console.WriteLine();
    }
    static void RunExperiment(Experiment test, int loop)
    {
        // avoid any GC interruptions
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();

        double val = 0;
        var watch = Stopwatch.StartNew();
        for (int i = 0; i < loop; i++)
            val = test.GetValue();
        watch.Stop();
        if (val != 3.0) Console.WriteLine("FAIL!");
        Console.WriteLine("{0}\t{1}ms", test.GetType().Name,
            watch.ElapsedMilliseconds);

    }

}
于 2013-04-12T08:54:14.843 に答える
6

同時実行の読み取りのみを測定することは誤解を招きます。キャッシュは、実際の使用例よりも桁違いに優れた結果をもたらします。そこで、Marc の例に SetValue を追加しました。

using System;
using System.Diagnostics;
using System.Threading;

abstract class Experiment
{
    public abstract double GetValue();
    public abstract void SetValue(double value);
}

class Example1 : Experiment
{
    private readonly object _sync = new object();
    private double _value = 3;
    public override double GetValue()
    {
        lock (_sync)
        {
            return _value;
        }
    }

    public override void SetValue(double value)
    {
        lock (_sync)
        {
            _value = value;
        }

    }

}
class Example2 : Experiment
{
    private readonly object _sync = new object();
    private double _value = 3;
    public override double GetValue()
    {
        lock (_sync)
        {
            return _value;
        }
    }

    public override void SetValue(double value)
    {
        lock (_sync)
        {
            _value = value;
        }
    }

}



class Example3 : Experiment
{
    private readonly object _sync = new object();
    private double _value = 3;
    public override double GetValue()
    {
        double result = 0;
        lock (_sync)
        {
            result = _value;
        }
        return result;
    }

    public override void SetValue(double value)
    {
        lock (_sync)
        {
            _value = value;
        }
    }
}

class CompareExchange : Experiment
{
    private double _value = 3;
    public override double GetValue()
    {
        return Interlocked.CompareExchange(ref _value, 0, 0);
    }

    public override void SetValue(double value)
    {
        Interlocked.Exchange(ref _value, value);
    }
}
class ReadUnsafe : Experiment
{
    private long _value = DoubleToInt64(3);
    static unsafe long DoubleToInt64(double val)
    {   // I'm mainly including this for the field initializer
        // in real use this would be manually inlined
        return *(long*)(&val);
    }
    public override unsafe double GetValue()
    {
        long val = Interlocked.Read(ref _value);
        return *(double*)(&val);
    }

    public override void SetValue(double value)
    {
        long intValue = DoubleToInt64(value);
        Interlocked.Exchange(ref _value, intValue);
    }
}
class UntypedBox : Experiment
{
    // references are always atomic
    private volatile object _value = 3.0;
    public override double GetValue()
    {
        return (double)_value;
    }

    public override void SetValue(double value)
    {
        object valueObject = value;
        _value = valueObject;
    }
}
class TypedBox : Experiment
{
    private sealed class Box
    {
        public readonly double Value;
        public Box(double value) { Value = value; }

    }
    // references are always atomic
    private volatile Box _value = new Box(3);
    public override double GetValue()
    {
        Box value = _value;
        return value.Value;
    }

    public override void SetValue(double value)
    {
        Box boxValue = new Box(value);
        _value = boxValue;
    }
}
static class Program
{
    static void Main()
    {
        // once for JIT
        RunExperiments(1);
        // three times for real
        RunExperiments(5000000);
        RunExperiments(5000000);
        RunExperiments(5000000);
    }
    static void RunExperiments(int loop)
    {
        Console.WriteLine("x{0}", loop);
        RunExperiment(new Example1(), loop);
        RunExperiment(new Example2(), loop);
        RunExperiment(new Example3(), loop);
        RunExperiment(new CompareExchange(), loop);
        RunExperiment(new ReadUnsafe(), loop);
        RunExperiment(new UntypedBox(), loop);
        RunExperiment(new TypedBox(), loop);
        Console.WriteLine();
    }
    static void RunExperiment(Experiment test, int loop)
    {
        // avoid any GC interruptions
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();

        int threads = Environment.ProcessorCount;

        ManualResetEvent done = new ManualResetEvent(false);

        // Since we use threads, divide the original workload
        //
        int workerLoop = Math.Max(1, loop / Environment.ProcessorCount);
        int writeRatio = 1000;
        int writes = Math.Max(workerLoop / writeRatio, 1);
        int reads = workerLoop / writes;

        var watch = Stopwatch.StartNew();

        for (int t = 0; t < Environment.ProcessorCount; ++t)
        {
            ThreadPool.QueueUserWorkItem((state) =>
                {
                    try
                    {
                        double val = 0;

                        // Two loops to avoid comparison for % in the inner loop
                        //
                        for (int j = 0; j < writes; ++j)
                        {
                            test.SetValue(j);
                            for (int i = 0; i < reads; i++)
                            {
                                val = test.GetValue();
                            }
                        }
                    }
                    finally
                    {
                        if (0 == Interlocked.Decrement(ref threads))
                        {
                            done.Set();
                        }
                    }
                });
        }
        done.WaitOne();
        watch.Stop();
        Console.WriteLine("{0}\t{1}ms", test.GetType().Name,
            watch.ElapsedMilliseconds);

    }
}

結果は、1000:1 の読み取り:書き込み比率で次のようになります。

x5000000
Example1        353ms
Example2        395ms
Example3        369ms
CompareExchange 150ms
ReadUnsafe      161ms
UntypedBox      11ms
TypedBox        9ms

100:1 (読み取り:書き込み)

x5000000
Example1        356ms
Example2        360ms
Example3        356ms
CompareExchange 161ms
ReadUnsafe      172ms
UntypedBox      14ms
TypedBox        13ms

10:1 (読み取り:書き込み)

x5000000
Example1        383ms
Example2        394ms
Example3        414ms
CompareExchange 169ms
ReadUnsafe      176ms
UntypedBox      41ms
TypedBox        43ms

2:1 (読み取り:書き込み)

x5000000
Example1        550ms
Example2        581ms
Example3        560ms
CompareExchange 257ms
ReadUnsafe      292ms
UntypedBox      101ms
TypedBox        122ms

1:1 (読み取り:書き込み)

x5000000
Example1        718ms
Example2        745ms
Example3        730ms
CompareExchange 381ms
ReadUnsafe      376ms
UntypedBox      161ms
TypedBox        200ms

*値は常に上書きされるため、書き込み時に不要な ICX 操作を削除するようにコードを更新しました。また、読み取り数をスレッドで割る計算式を修正しました (同じ作業)。

于 2013-04-12T12:22:51.707 に答える