【问题标题】:C# thread-safe getter performance differencesC# 线程安全的 getter 性能差异
【发布时间】:2013-04-12 22:34:52
【问题描述】:

我正在编写一个线程安全的对象,它基本上代表一个 double 并使用锁来确保安全读写。我在一段代码中使用了许多这些对象(20-30),每秒读取和写入它们 100 次,并且我正在测量每个时间步长的平均计算时间。我开始寻找一些用于实现我的 getter 的选项,在运行了许多测试并收集了许多样本以平均计算时间测量后,我发现某些实现的性能始终优于其他实现,但不是我期望的实现。

实现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 一样快,如果不快的话,因为我只是删除了对 double 结果的分配,无论如何都会被覆盖,所以这是不必要的。

我的问题是:谁能解释为什么这 3 种实现具有我所测量的相对性能?这对我来说似乎违反直觉,我真的很想知道为什么。

我意识到这些差异并不大,但每次我运行测试时它们的相对测量值都是一致的,每次测试收集数千个样本以平均计算时间。另外,请记住我正在做这些测试,因为我的应用程序需要非常高的性能,或者至少我可以合理地得到它。我的测试用例只是一个小测试用例,我的代码在发布时的性能很重要。

编辑:请注意,我正在使用 MonoTouch 并在 iPad Mini 设备上运行代码,所以它可能与 c# 无关,而与 MonoTouch 的交叉编译器有关。

【问题讨论】:

  • 考虑到这些是多么接近相同(即使从 IL 的角度来看,我也不希望它们在计算上有很大不同)以及基准时间有多接近,我怀疑罪魁祸首是您的测试方法。它是为release 模式编译的吗?你是如何进行基准测试的?计算机上的其他进程是否可能会干扰处理资源/时间?您是否在模拟锁争用,如果是,如何模拟?编辑:另外,我怀疑您的实际应用程序在lock 块中有更复杂的工作?因为按原样,锁对我来说似乎是多余的。
  • 1) 如果不争用锁,测试是没有意义的。
  • 请显示您用于测试的代码
  • 2) 好好看看System.Threading.Interlocked
  • 在 x86 Release 模式下编译时,前两个实现具有相同本机代码。第三个实现也一样,除了额外的fldzfstp指令将变量初始化为0。

标签: c# performance locking


【解决方案1】:

坦率地说,这里还有其他更好的方法。以下输出(忽略用于 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);

    }

}

【讨论】:

  • 可以去掉TypedBox.GetValue中的unsafe修饰符吗?
  • @Guillaume86 抱歉,复制粘贴错误;是 - 将编辑;它不会改变数字
  • @MarcGravell 感谢您进行非常有趣的比较!我有一个问题,TypedBox 实现是否因为 readonly 关键字而只允许读取?我确实需要安全地读写,我只是没有包含编写代码,因为我没有在我的原始帖子中测试它,尽管我在我的 SetValue 方法中使用了相同的锁定机制。如果在有类型和无类型的 Box 示例中添加方法 SetValue,这些实现仍然是线程安全的吗?我看不出是什么让这些类中的阅读成为排他性/原子性的,我错过了什么吗? (C# 新手!)
  • @Camputer 给你写一个新的 Box(value) - 虽然考虑一下你可能需要在两个盒子示例中的两个字段中添加 volatile 关键字。语言规范保证对引用的访问始终是原子的,并且对象是不可变的,保证一旦我们取消引用该值就可以了。
【解决方案2】:

仅测量并发读取具有误导性,您的缓存将为您提供比实际用例更好的结果数量级。所以我在 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 操作,因为该值总是被覆盖。还修复了计算除以线程的读取数的公式(同样的工作)。

【讨论】:

  • 感谢您在其中添加 SetValue,因为我认为它会有所作为。问题:在 TypedBox 实现中创建新的 Box 实例不是很昂贵吗?这就是为什么随着结果中写入比例的增加,它的执行时间开始慢慢增加的原因吗?
  • 要回答您的问题,实际上必须profile 代码。
  • 非常彻底的跟进,谢谢。对于信息,我的意思是在盒子/打字盒示例中添加“易失性” - 否则可能会得到脏读 - 我的错误。
  • @RemusRusanu 我的意思是一般来说,如果我正在执行大量读写运行时间敏感的计算,我的直觉告诉我尽可能避免实例化新对象。这有意义吗?
  • @Camputer:确实如此。我自己对结果感到有些困惑,因为盒子示例对我来说似乎太好了。但请记住,此示例仅显示基本安全的原子。线程正在覆盖彼此的值,这是最后写入获胜的情况,这不太可能是实际使用情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-12-12
  • 2023-04-09
  • 1970-01-01
  • 2021-03-07
  • 1970-01-01
  • 2011-04-06
  • 2011-12-17
相关资源
最近更新 更多