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

7

当我优化一个程序时,发现了一些奇怪的性能结果,这些结果在下面的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 而不是字符串运行相同的基准测试时,我得到的运行时间大致相同。此外,我使用 BenchmarkDotNet 的反汇编诊断器查看生成的实际汇编代码,并发现了一些有趣的东西:对于 int/long 版本,我可以清楚地看到 xchg 和 cmpxchg 指令,但对于字符串,我看到了调用 Interlocked.Exchange/Interlocked.CompareExchange 方法的指令...!

编辑2:在 coreclr 中打开了一个问题:https://github.com/dotnet/coreclr/issues/16051


1
你的CompareExchange没有执行交换,因为“_s!= null”。 - Blorgbeard
2
这看起来像是优化框架代码的副作用。在4.x版本的某个地方添加。这是一个相当聪明的技巧,Jitter对COMInterlocked :: CompareExchangeGeneric()的调用进行了猴子补丁,使其变成了COMInterlocked :: CompareExchangeObject()。虽然在C#中是非法的,但在这种情况下是安全的,因为实际类型并不重要,它只交换对象引用,并且该方法被限制为引用类型。然而,他忽视了一件事,他本可以用Exchange()做完全相同的事情。可能不在他的待办列表上 :) - Hans Passant
所以一个好的建议是优先使用CompareExchange()。微软程序员在BCL类型上工作时似乎意识到了这一点,在框架代码的许多地方都使用了CompareExchange而不是Exchange。 - Hans Passant
如果以上不清楚 - 如果您想在当前使用 Exchange 的地方使用 CompareExchange,则需要预先了解目标存储的值的内容。如果你没有这些内容的预先知识(这是普遍适用的),那么你就需要编写一个循环。因此,我不明白为什么你会开始对这些事情进行基准测试。 - Damien_The_Unbeliever
@Damien_The_Unbeliever 首先,我并不需要理由来对事物进行基准测试,这可能只是出于好奇。其次,我正在考虑做与您所说相反的事情:我有一些代码目前使用CompareAndExchange,但实际上并不需要关心当前值,因此Exchange更合适(这就是为什么我进行基准测试的原因)。最后,我的Exchange用法将在一个非常注重性能的循环中,我真的不知道为什么您会认为Exchange不值得优化(毕竟它有一个CPU操作)。 - Shay Rojansky
显示剩余7条评论
2个回答

8

针对我的评论,这似乎与Exchange的通用重载版本有关。

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

问题仍然存在,即为什么仅将解析到通用重载版本会减慢Exchange。阅读Interlocked源代码时,看起来在CompareExchange<T>中实现了一种hack以使其更快。关于CompareExchange<T>的源代码注释如下:

 * 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<T> 中没有类似的评论,它也使用了“极慢”的 __makeref,这可能是您看到此意外行为的原因。

当然,这都是我的解释,您实际上需要 .NET 团队的人来确认我的怀疑。


1

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