【问题标题】:Performance of Interlocked.IncrementInterlocked.Increment 的性能
【发布时间】:2010-11-05 06:22:45
【问题描述】:

对于各种平台上的整数和长整数,Interlocked.Increment(ref x)x++ 快还是慢?

【问题讨论】:

  • 正如其他人指出的那样,这不是一回事。也就是说,根据msdn.microsoft.com/en-us/magazine/cc163726.aspx,Interlocked.Increment 需要大约 14nS(或每秒大约 71'000'000),所以我不会太担心性能
  • Interlocked.Increment 旨在用于线程环境下

标签: .net performance interlocked interlocked-increment


【解决方案1】:

我的表现测试:

易失性:65,174,400

锁定:62,428,600

联锁:113,248,900

TimeSpan span = TimeSpan.FromSeconds(5);

object syncRoot = new object();
long test = long.MinValue;

Do(span, "volatile", () => {

    long r = Thread.VolatileRead(ref test);

    r++;

    Thread.VolatileWrite(ref test, r);
});

Do(span, "lock", () =>
{
    lock (syncRoot)
    {
        test++;
    }
});

Do(span, "interlocked", () =>
{
    Interlocked.Increment(ref test);
});

【讨论】:

  • 它运行该方法 n 次,直到达到时间跨度
  • 那么,等等,在这种情况下越多越好?您能否具体说明您的指标是什么?
  • 不以这种方式使用VolatileReadVolatileWrite 会让您面临lockInterlocked.Increment 避免的相同竞争条件吗?
【解决方案2】:

它比较慢,因为它强制动作以原子方式发生,并且它充当内存屏障,消除了处理器围绕指令重新排序内存访问的能力。

当您希望操作在可以在线程之间共享的状态上具有原子性时,您应该使用 Interlocked.Increment - 它并不打算完全替代 x++。

【讨论】:

    【解决方案3】:

    根据我们的经验,Windows 上的 InterlockedIncrement() 等具有相当大的影响。在一个示例案例中,我们能够消除联锁并改用 ++/--。仅此一项就将运行时间从 140 秒减少到 110 秒。我的分析是互锁强制内存往返(否则其他内核怎么能看到它?)。一级缓存读/写大约需要 10 个时钟周期,但内存读/写更像 100 个。

    在这个示例中,我估计递增/递减操作的数量约为 10 亿次。所以在 2Ghz CPU 上,这对于 ++/-- 来说是 5 秒,对于互锁来说是 50 秒。将差异分散到多个线程中,接近 30 秒。

    【讨论】:

    • Micrsoft 说:在 msdn.microsoft.com/en-us/library/windows/desktop/… 中,InterlockedIncrement 被测量为需要 36-90 个周期
    • 36 听起来适合无争议的操作,我在 Core i7 上测量了大约 120 次有争议的操作,但也许我搞砸了?无论如何,“互锁强制内存往返(否则其他内核怎么能看到它?)。L1 缓存读/写大约 10 个时钟周期......” - 足以将该页面标记为已更改并且仅从 L1 刷新到内存如果另一个核心需要看到它,那么无争议的操作可以更接近频谱的 10 端(在 36 处)而不是 100+....
    【解决方案4】:

    想一想,您会发现Increment 调用不会比增量运算符的简单应用更快。如果是这样,那么编译器对自增运算符的实现将在内部调用Increment,它们会执行相同的操作。

    但是,您可以通过自己的测试看到,它们的表现并不相同。

    这两个选项有不同的目的。一般使用增量运算符。当您需要原子操作并且您确定该变量的所有其他用户也在使用互锁操作时,请使用Increment。 (如果他们不是都合作,那么这并没有真正的帮助。)

    【讨论】:

    • 不,它不会 - Interlocked.Increment 不能在属性上调用,而 ++ 运算符可以。因此,++ 将无法调用它。
    • 更准确地说,Increment 需要一个 ref int(或 long); ++ 采用非引用 int(或 long)
    • 编译器当然可以通过增量实现++。它不会使用简单的“调用”指令来实现,但可以使用编译器引入的临时指令来完成。关键是编译器使用了快速可用的递增数字的方法。如果有更快的东西,编译器会改用它。
    【解决方案5】:

    速度较慢。但是,这是我所知道的在标量变量上实现线程安全的最常用的通用方法。

    【讨论】:

    • volatile 在标量上的性能更高,但缺点是需要使用良好的编码实践。
    • 小心易失性;某些处理器架构 (x86/x64) 能够重新排序对内存的访问,无论该内存是否被编译器标记为易失性。
    • @DrewHoskins 行为如规范所述,编译器需要正确实现规范。为 .Net 编写代码的程序员永远不必考虑底层 ISA 的内存模型(可能除了性能方面)。如果以某种方式发生非法重新排序,这是一个严重的编译器错误。
    • @Abel volatile++ 仍然是不是原子的/不是线程安全的,而Interlocked.Increment(或@987654325 @ 和 ++) 是线程安全的。此外,volatile 对于任何一种有效的线程安全方法都是不必要的..
    • @user2864740,我不记得为什么我写了我10年前所做的事情,当时可能还有其他cmet现在被删除了。但是当您希望变量随时更改并且不需要或不可能使用锁时,应该使用 volatile 关键字。我在评论中没有使用术语线程安全,但我认为它是安全的,因为你不会遇到死锁,但正确使用它很棘手。当你想要原子读/写时,你不应该使用 volatile。
    【解决方案6】:

    它总是会更慢,因为它必须执行 CPU 总线锁定而不是仅更新寄存器。然而,现代 CPU 实现了接近寄存器的性能,因此即使在实时处理中也可以忽略不计。

    【讨论】:

    • 虽然 X86 CPU 在互锁操作期间执行总线锁,但并非所有提供互锁操作的 CPU 都需要总线锁。一些 CPU 能够发出信号,表明它们保留了单个高速缓存行,并且可以在该高速缓存行上执行互锁操作而无需总线锁。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-27
    • 2023-03-17
    • 2011-08-07
    • 1970-01-01
    相关资源
    最近更新 更多