Interlocked.Exchange<T> 比 Interlocked.CompareExchange<T> 慢吗?

Posted

技术标签:

【中文标题】Interlocked.Exchange<T> 比 Interlocked.CompareExchange<T> 慢吗?【英文标题】:Interlocked.Exchange<T> slower than Interlocked.CompareExchange<T>? 【发布时间】:2018-01-26 21:51:29 【问题描述】:

我在优化程序时遇到了一些奇怪的性能结果,这些结果显示在以下 BenchmarkDotNet 基准测试中:

string _s, _y = "yo";

[Benchmark]
public void Exchange() => Interlocked.Exchange(ref _s, null);

[Benchmark]
public void CompareExchange() => Interlocked.CompareExchange(ref _s, _y, null);

结果如下:

BenchmarkDotNet=v0.10.10, OS=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.192)
Processor=Intel Core i7-6700HQ CPU 2.60GHz (Skylake), ProcessorCount=8
Frequency=2531248 Hz, Resolution=395.0620 ns, Timer=TSC
.NET Core SDK=2.1.4
  [Host]     : .NET Core 2.0.5 (Framework 4.6.26020.03), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.5 (Framework 4.6.26020.03), 64bit RyuJIT

          Method |      Mean |     Error |    StdDev |
---------------- |----------:|----------:|----------:|
        Exchange | 20.525 ns | 0.4357 ns | 0.4662 ns |
 CompareExchange |  7.017 ns | 0.1070 ns | 0.1001 ns |

似乎Interlocked.Exchange 的速度是Interlocked.CompareExchange 的两倍多——这令人困惑,因为它应该做的工作更少。除非我弄错了,否则两者都应该是 CPU 操作。

有人对为什么会发生这种情况有很好的解释吗?这是 CPU 操作的实际性能差异还是 .NET Core 包装它们的方式存在问题?

如果是这种情况,最好直接避免Interlocked.Exchange() 并尽可能使用Interlocked.CompareExchange()

编辑:另一件奇怪的事情:当我使用 int 或 long 而不是 string 运行相同的基准测试时,我得到或多或少相同的运行时间。此外,我使用 BenchmarkDotNet 的反汇编程序诊断器查看正在生成的实际程序集,并发现了一些有趣的东西:使用 int/long 版本我可以清楚地看到 xchg 和 cmpxchg 指令,但我看到调用 Interlocked.Exchange/Interlocked 的字符串。 CompareExchange 方法...!

EDIT2:在 coreclr 中打开的问题:https://github.com/dotnet/coreclr/issues/16051

【问题讨论】:

您的 CompareExchange 没有进行交换,因为 _s != null 是的,检查docs 你的论点是错误的 哦等等,它确实等于null。你的 string _s, y = "yo"; 语法让我很困惑。但这意味着您的测试正在做不同的事情。可能是两个单独的错误。 这似乎与Exchange 的泛型重载有关。如果您使用键入为object_s_y 运行相同的代码,则两种方法的执行情况大致相同。差异似乎仅在方法调用解析为Interlocked.Exchange&lt;T&gt; 时才显现出来。这也是您没有直接看到xchg 指令的原因,存在一定程度的间接性。但问题仍然存在,为什么它显然只会放慢速度Exchange 这看起来像是努力优化框架代码的副作用。在版本 4.x 中的某处添加。非常聪明的 hack,抖动猴子修补了对 COMInterlocked::CompareExchangeGeneric() 的调用到 COMInterlocked::CompareExchangeObject()。这在 C# 中是非法的,但在这种情况下是安全的,因为实际类型无关紧要,它只交换对象引用并且方法被限制为引用类型。然而,他忽略的是,他可以用 Exchange() 做同样的事情。可能不在他的待办事项清单上:) 【参考方案1】:

根据我的评论,这似乎是Exchange 的通用重载的问题。

如果您完全避免泛型重载(将_s_y 的类型更改为object),性能差异就会消失。

问题仍然存在,为什么解决泛型重载只会减慢Exchange。通读Interlocked 源代码,似乎在CompareExchange&lt;T&gt; 中实现了一个hack 以使其更快。 CompareExchange&lt;T&gt;的源码注释如下:

 * CompareExchange<T>
 * 
 * Notice how CompareExchange<T>() uses the __makeref keyword
 * to create two TypedReferences before calling _CompareExchange().
 * This is horribly slow. Ideally we would like CompareExchange<T>()
 * to simply call CompareExchange(ref Object, Object, Object); 
 * however, this would require casting a "ref T" into a "ref Object", 
 * which is not legal in C#.
 * 
 * Thus we opted to cheat, and hacked to JIT so that when it reads
 * the method body for CompareExchange<T>() it gets back the
 * following IL:
 *
 *     ldarg.0 
 *     ldarg.1
 *     ldarg.2
 *     call System.Threading.Interlocked::CompareExchange(ref Object, Object, Object)
 *     ret
 *
 * See getILIntrinsicImplementationForInterlocked() in VM\JitInterface.cpp
 * for details.

Exchange&lt;T&gt; 中没有类似的评论,它还利用了“非常慢”__makeref,所以这可能是您看到这种意外行为的原因。

所有这些当然是我的解释,你实际上需要 .NET 团队的某个人来真正证实我的怀疑。

【讨论】:

【参考方案2】:

此问题现已在较新版本的 .Net Core 上得到修复:

https://github.com/dotnet/coreclr/issues/16051

【讨论】:

以上是关于Interlocked.Exchange<T> 比 Interlocked.CompareExchange<T> 慢吗?的主要内容,如果未能解决你的问题,请参考以下文章

Interlocked.exchange 作为锁

Interlocked.Exchange() 具有依赖于读取锁定变量的自定义条件

引用分配是原子的,那么为啥需要 Interlocked.Exchange(ref Object, Object)?

如何为 C# 中的枚举类型应用 InterLocked.Exchange?

如何执行双重原子读取?

C#ThreadInterlocked 轻量级锁