.NET JIT 编译器可变优化

.NET JIT compiler volatile optimizations

https://msdn.microsoft.com/en-us/magazine/jj883956.aspx

Consider the polling loop pattern:

private bool _flag = true; 
public void Run() 
{
    // Set _flag to false on another thread
    new Thread(() => { _flag = false; }).Start();
    // Poll the _flag field until it is set to false
    while (_flag) ;
    // The loop might never terminate! 
} 

In this case, the .NET 4.5 JIT compiler might rewrite the loop like this:

if (_flag) { while (true); } 

In the single-threaded case, this transformation is entirely legal and, in general, hoisting a read out of a loop is an excellent optimization. However, if the _flag is set to false on another thread, the optimization can cause a hang.

Note that if the _flag field were volatile, the JIT compiler would not hoist the read out of the loop. (See the “Polling Loop” section in the December article for a more detailed explanation of this pattern.)

如果我锁定 _flag,JIT 编译器是否仍会像上面显示的那样优化代码,或者只会使其 volatile 停止优化?

Eric Lippert 对 volatile 的评价如下:

Frankly, I discourage you from ever making a volatile field. Volatile fields are a sign that you are doing something downright crazy: you're attempting to read and write the same value on two different threads without putting a lock in place. Locks guarantee that memory read or modified inside the lock is observed to be consistent, locks guarantee that only one thread accesses a given chunk of memory at a time, and so on. The number of situations in which a lock is too slow is very small, and the probability that you are going to get the code wrong because you don't understand the exact memory model is very large. I don't attempt to write any low-lock code except for the most trivial usages of Interlocked operations. I leave the usage of "volatile" to real experts.

总结一下:谁保证上面提到的优化不会破坏我的代码?只有volatile?还有 lock 声明?或者别的什么?

Eric Lippert 不鼓励您使用 volatile 一定还有其他东西吗?


反对者: 我感谢对这个问题的每一个反馈。特别是如果你不赞成,我想听听你为什么认为这是一个糟糕的问题。


布尔变量不是线程同步原语:这个问题是一个一般性问题。什么时候编译器不做优化?


重复:这个问题明确是关于优化的。您链接的那个没有提到优化。

This question is explicitly about optimizations

这不是正确的观点。重要的是如何指定语言的行为。 JIT只会在不违反规范的约束下进行优化。因此,优化对程序是不可见的。这个问题中代码的问题不是它正在被优化。问题是规范中没有任何内容强制程序正确。为了解决这个问题,您 而不是 关闭优化或以某种方式与编译器通信。您使用保证您需要的行为的原语。


您无法锁定 _flaglockMonitor class 的语法。 class 基于堆上的对象进行锁定。 _flag 是一个不可锁定的布尔值。

为了取消循环,这些天我会使用 CancellationTokenSource。它在内部使用易失性访问,但对您隐藏了。循环轮询 CancellationToken 并通过调用 CancellationTokenSource.Cancel() 取消循环。这是非常自我记录且易于实施的。

您还可以将对 _flag 的任何访问权限封装在一个锁中。看起来像这样:

object lockObj = new object(); //need any heap object to lock
...

while (true) {
 lock (lockObj) {
  if (_flag) break;
 }
 ...
}

...

lock (lockObj) _flag = true;

您也可以使用 volatile。 Eric Lippert 说得非常正确,如果没有必要,最好不要接触硬核线程。

当您使用 lockInterlocked Operations 时,您是在告诉编译器块或内存位置存在数据竞争,并且它不能对访问这些位置做出假设。因此,编译器放弃了它可以在无数据竞争环境中执行的优化。这个隐含的合同还意味着您告诉编译器您将以适当的无数据争用方式访问这些位置。

让我们回答被问到的问题:

Will the JIT compiler still optimize the code as shown above if I lock _flag or will only making it volatile stop the optimization?

好吧,我们不回答被问到的问题,因为那个问题太复杂了。让我们把它分解成一系列不太复杂的问题。

Will the JIT compiler still optimize the code as shown above if I lock _flag?

简短回答:lockvolatile 提供 更强的 保证,所以不,不允许抖动解除读取如果 _flag 的读取周围有锁,则循环。当然锁也得围绕write。锁只有在 随处.

时才有效
private bool _flag = true; 
private object _flagLock = new object();
public void Run() 
{
  new Thread(() => { lock(_flaglock) _flag = false; }).Start();
  while (true)
    lock (_flaglock)
      if (!_flag)
        break;
} 

(当然,我注意到这是等待一个线程向另一个线程发送信号的非常糟糕的方式。永远不要坐在一个紧密的循环中轮询标志!像一个明智的人一样使用等待句柄。)

You said locks were stronger than volatiles; what does that mean?

读取 volatiles 会阻止某些操作及时移动。写入 volatiles 会阻止某些操作及时移动。锁会阻止 more 操作及时移动。这些预防语义称为 "memory fences"——基本上,volatiles 引入了半栅栏,锁引入了全栅栏。

阅读有关特殊副作用的 C# 规范部分了解详细信息。

一如既往,我会提醒您,挥发物不能为您提供全球新鲜度保证。在多线程 C# 编程中没有变量的 "the latest" 值这样的东西,因此易失性读取不会为您提供 "the latest" 值,因为它不存在。存在 "latest" 值的想法意味着始终观察到读取和写入在时间上具有全局一致的顺序,这是错误的。线程仍然可能不同意易失性读取和写入的顺序。

Locks prevent this optimization; volatiles prevent this optimization. Are those the only thing which prevents the optimization?

没有。也可以使用Interlocked操作,也可以显式引入内存栅栏

Do I understand enough of this to use volatile correctly?

没有。

What should I do?

首先不要编写多线程程序。一个程序中的多个控制线程不是一个好主意。

如果必须,请不要跨线程共享内存。将线程用作低成本进程,并且仅在您有空闲 CPU 可以执行 CPU 密集型任务时才使用它们。对所有 I/O 操作使用单线程异步。

如果您必须跨线程共享内存,请使用您可用的最高级别的编程构造,而不是最低级别。使用 CancellationToken 表示在异步工作流的其他地方取消的操作。

C# volatile 关键字实现了所谓的获取和释放语义,因此用它来进行简单的线程同步是完全合法的。并且任何符合标准的 JIT 引擎都不应该将其优化掉。

当然,这是一种虚假的语言特性,因为 C/C++ 具有不同的语义,而大多数程序员可能已经习惯了这种语义。因此,"volatile" 的 C# 特定和 Windows 特定(ARM 体系结构除外)用法有时令人困惑。