Interlocked.Increment的性能表现

22

在不同平台上,对于int和long类型,Interlocked.Increment(ref x)x++哪个更快或更慢?


正如其他人所指出的那样,这不是同一件事。话虽如此,根据http://msdn.microsoft.com/en-us/magazine/cc163726.aspx的说法,Interlocked.Increment需要大约14纳秒(或每秒约71,000,000次),因此我不会过于担心性能问题。 - smirkingman
1
Interlocked.Increment 旨在在多线程环境下使用。 - EProgrammerNotFound
6个回答

44

由于Interlocked.Increment强制操作以原子方式进行,并且充当内存屏障,消除了处理器在指令周围重新排序内存访问的能力,因此它速度较慢。

当您想要在线程之间共享的状态上执行原子操作时,应该使用Interlocked.Increment - 它不是x++的完全替代品。


18

根据我们的经验,在Windows上使用InterlockedIncrement()等函数会产生相当大的影响。在一个样例中,我们能够消除互锁并使用++/--代替。这单一操作就将运行时间从140秒减少到110秒。我的分析是,互锁强制进行内存往返(否则其他核心如何看到它?)。L1缓存读/写约为10个时钟周期,但内存读/写需要更多的时间,约为100个时钟周期。

在这个样例中,我估计增量/减量操作的数量约为10亿次。因此,在2Ghz的CPU上,++/--需要约5秒钟,而互锁需要约50秒钟。将差异分散到几个线程中,就接近30秒了。


9
微软表示:在http://msdn.microsoft.com/en-us/library/windows/desktop/ee418650(v=vs.85).aspx中,已测量InterlockedIncrement的执行时间为36-90个时钟周期。 - Lothar
2
36听起来适合于无争议操作,在Core i7上进行高度争议的操作我测量了大约120次,但也许我搞砸了?无论如何,“交错强制进行内存往返(否则其他核心怎么能看到它?)。L1缓存读/写大约需要10个时钟周期……”-这足以将该页面标记为已更改,并仅在另一个核心需要查看它时从L1刷新到内存,因此无争议操作可以更接近谱系的10端(在36处),而不是100+。 - Tony Delroy

7
思考一下,你会意识到Increment调用不能比简单的递增运算符应用更快。如果是这样的话,那么编译器对递增运算符的实现将在内部调用Increment,它们将执行相同的操作。
但是,通过自己的测试,您可以看到它们执行的不同。
这两个选项有不同的目的。通常使用递增运算符。当您需要操作是原子性的,并且您确定该变量的所有其他用户也正在使用交错操作时,请使用Increment。(如果他们没有全部合作,则实际上并没有帮助)。

不会的 - Interlocked.Increment不能在属性上调用,而++运算符可以。因此,++无法调用它。 - SLaks
1
更准确地说,Increment 接受一个 ref int(或 long);++ 接受一个非 ref int(或 long)。 - SLaks
3
编译器可以通过增量实现 ++ 运算符。虽然不会使用简单的 "call" 指令来实现,但是编译器可以通过引入临时变量来完成。关键是编译器使用最快的可用方法来增加一个数字;如果有更快的方法,编译器就会使用它。 - Rob Kennedy

5

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


1
volatile 在标量上的表现更佳,但缺点是需要使用良好的编程实践来发挥其作用。 - Abel
2
注意volatile关键字;一些处理器架构(如x86/x64)具有重新排序内存访问的能力,无论该内存是否被标记为编译器的volatile。 - Drew Hoskins
@DrewHoskins 行为与规范所述一致,编译器需要正确实现规范。编写 .Net 程序的程序员无需考虑底层 ISA 的内存模型(也许只需考虑性能)。如果出现非法重排序,那就是严重的编译器错误。 - Max Barraclough
@Abel 在使用 ++ 运算符时,即使加上 volatile 关键字,仍然不是原子操作 / 不是线程安全的。而使用 Interlocked.Increment(或者 lock++)则是线程安全的。此外,在这两种有效的线程安全方法中,使用 volatile 是不必要的。 - user2864740
1
@user2864740,我不记得10年前为什么写了这个代码,当时可能有其他的注释已经被删除了。但是当你期望一个变量随时会改变且不需要或不可能使用锁时,应该使用volatile关键字。在我的注释中没有使用“线程安全”这个术语,但我认为它是安全的,因为你不会遇到死锁,但是正确使用它很棘手。当你想要原子读/写时,不应该使用volatile。 - Abel

3

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


4
在执行Interlocked操作期间,X86 CPU会执行总线锁定(buslock),但并非所有提供Interlocked操作的CPU都需要总线锁定。一些CPU能够发出信号,表示它们已经保留了一条单独的缓存行,可以在该缓存行上执行Interlocked操作而无需总线锁定。 - Adisak

2

我的性能测试:

volatile(易失性变量):65,174,400

lock(锁):62,428,600

interlocked(交互式):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次,直到达到时间跨度。 - MastsrOfDesaster
5
所以,等等,这种情况下更多就是更好的?请问您能具体说明一下您的指标是什么吗? - Kevin Fee
6
以这种方式使用 VolatileReadVolatileWrite,会不会让你面临与 lockInterlocked.Increment 可以避免的相同竞争条件呢? - ta.speot.is
你的Volatile示例不是线程安全的。 - nim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接