C# 线程安全的 getter 性能差异

标签 c# performance locking

我正在编写一个线程安全对象,它基本上代表一个 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 一样快,如果不比实现 3 快的话,因为我只是删除对无论如何都会被覆盖的 double 结果的赋值,所以这是不必要的。

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

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

编辑:请注意,我正在使用 MonoTouch 并在 iPad Mini 设备上运行代码,因此它可能与 c# 无关,更多的是与 MonoTouch 的交叉编译器相关。

最佳答案

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

    }

}

关于C# 线程安全的 getter 性能差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15950704/

相关文章:

c# - Task.Wait 在 OperationCanceledException 情况下的意外行为

c# - 避免在 try/catch block 中出现警告 "variable is declared but never used"

java - OutOfMemoryException - 使用 Glide 的最佳方式

Swift 弱引用比强引用慢得多

.net - 使用 Datetime.Now 具有不同文件名的 TPL

node.js - Node Redis - 使用 EX 和 NX 设置?

c# - 使用命名参数减小值不会更改该值

c# - 如何解决 IEnumerator(Unity, C#) 的错误?

c++ - SDL2 - 颜色快速变化时性能会受到奇怪影响

java - 顺序和并行处理