【问题标题】:Can compiler sometimes cache variable declared as volatile编译器有时可以缓存声明为 volatile 的变量吗
【发布时间】:2012-09-24 11:45:21
【问题描述】:

据我所知,编译器从不优化声明为volatile 的变量。但是,我有一个这样声明的数组。

volatile long array[8];

不同的线程对其进行读写。数组的元素仅由其中一个线程修改并由任何其他线程读取。但是,在某些情况下,我注意到即使我从线程修改元素,读取它的线程也不会注意到更改。它继续读取相同的旧值,就好像编译器已将其缓存在某个地方一样。但是编译器原则上不应该缓存 volatile 变量,对吗?那怎么会这样呢。

注意:我没有使用volatile 进行线程同步,所以请不要再给我答案,例如使用锁或原子变量。我知道易失性、原子变量和互斥体之间的区别。另请注意,该架构是具有主动缓存一致性的 x86。此外,在它被另一个线程修改后,我读了足够长的时间。即使过了很长时间,读取线程也看不到修改后的值。

【问题讨论】:

  • volatile 不适用于线程。您需要使用互斥锁。
  • C++ volatile 中的 AFAIK 仅影响编译器优化,不会影响仍然可能发生的 CPU 重新排序。
  • 你的笔记不会影响我的答案,顺便说一句,可能不会影响其他人的答案。您可能不会认为您使用volatile 进行线程同步,但如果您希望它在不同线程中引入读取和写入之间的关系,那么事实上您是,因为根据定义,这就是线程同步 是.
  • @Eric:它明确说明了读者何时会看到更新的期望:“我在被另一个线程修改后读取了足够长的变量”。假设测试代码正在执行它的预期,这是 user1018562 和他的实现之间的争论,关于“足够长”的时间。他说有一个限制,实施显然说没有。该标准不会干预该论点,它没有说明编译器是否“原则上不应该缓存 volatile 变量”,因为代码存在数据竞争。
  • 我会尝试互斥锁。如果这样可以解决它,那么您可以预期这是一个缓存问题。如果这不能解决问题,那么您可以在其他地方寻找问题。

标签: c++ c multithreading x86 volatile


【解决方案1】:

但是编译器原则上不应该缓存 volatile 变量,对吧?

不,编译器原则上每次读/写变量时都必须读/写变量的地址。

[编辑:至少,它必须这样做,直到实现认为该地址的值是“可观察的”。正如 Dietmar 在他的回答中指出的那样,一个实现可能会声明“无法观察到”正常的记忆。对于使用调试器、mprotect 或标准范围之外的其他东西的人来说,这会让他们感到惊讶,但原则上它可以符合。]

在完全不考虑线程的 C++03 中,由实现来定义在线程中运行时“访问地址”的含义。像这样的细节被称为“记忆模型”。例如,Pthreads 允许每个线程缓存整个内存,包括易失性变量。 IIRC,MSVC 保证合适大小的 volatile 变量是原子的,并且它将避免缓存(相反,它将刷新到所有内核的单个一致缓存)。它提供这种保证的原因是因为在英特尔上这样做相当便宜 - Windows 只真正关心基于英特尔的架构,而 Posix 则关心更奇特的东西。

C++11 为线程定义了一个内存模型,它说这是一个数据竞争(即volatile 确保一个线程中的读取相对于写在另一个线程)。两个访问可以按特定顺序排序,按未指定顺序排序(标准可能会说“不确定顺序”,我不记得了),或者根本不排序。完全不排序是不好的——如果两个未排序访问中的任何一个是写入,那么行为是未定义的。

这里的关键是“我从一个线程修改一个元素,然后读取它的线程没有注意到变化”中隐含的“然后”。您假设操作是按顺序排列的,但事实并非如此。就读取线程而言,除非您使用某种同步,否则其他线程中的写入不一定发生。实际上比这更糟糕——你可能会认为我刚刚写的只是未指定的操作顺序,但实际上具有数据竞争的程序的行为是未定义的。

【讨论】:

  • 我知道你在说什么,但我运行程序足够长的时间注意到线程继续读取相同的值,即使它已被另一个线程更改很长时间。缓存一致性可能需要一些时间,但我猜它不会超过几微秒,实际上比我想象的要小得多。
  • @user1018562:关键是数据竞争是未定义的行为。它成为 UB 的一个动机是处理非连贯缓存,但一旦发生这种行为可能是任何事情,因为优化器可能依赖于在转换代码时没有数据竞争。要求您的代码没有数据竞争的目的是允许编译器对具有它们的代码执行不正确的转换。
  • @SteveJessop:人们过分强调“未定义的行为”。永远不会将行为指定为未定义。它不是简单地没有定义。不同之处在于 C 标准可能不定义行为,但它不会阻止另一个规范定义它。您不能从 C 没有定义行为的事实中得出结论,即我们不能期望特定硬件以某些方式运行。如果硬件在一小段时间后确实传播了变化,并且发生了变化,并且在经过一段时间后没有观察到变化,那么就有一个 bug。
  • @Eric:大部分是真的,但是由于提问者没有说我们在谈论什么编译器或代码,所以说结果应该是什么是一个巨大的猜测,不管 i> 的硬件。如果您不编写实际进行更改的代码(或使用编译器),那么拥有传播更改的硬件是没有用的。如果提问者问了一个不同的问题,那么我可能会给出不同的答案,而不是强调标准,而更多地强调特定编译器的行为。你是对的,可能有一个错误,几乎可以肯定在提问者的代码中。
  • @SteveJessop:不。问题不在于内存障碍。问题是程序集是否包含 C 抽象机访问对象的加载指令。这是 C 标准所要求的。
【解决方案2】:

C

volatile 的作用:

  • 如果变量是从外部源(硬件寄存器、中断、不同线程、回调函数等)修改的,则保证变量中的值是最新的。
  • 阻止对变量的读/写访问的所有优化。
  • 当编译器没有意识到线程/中断/回调被程序调用时,防止可能发生在多个线程/中断/回调函数之间共享的变量上的危险优化错误。 (这在各种有问题的嵌入式系统编译器中尤为常见,当您遇到此错误时,很难追查到。)

volatile 没有:

  • 它不保证原子访问或任何形式的线程安全。
  • 它不能用来代替互斥体/信号量/保护/临界区。它不能用于线程同步。

volatile 可以做什么或不可以做什么:

  • 编译器可能会也可能不会实现它以提供内存屏障,以防止在多核环境中出现指令缓存/指令管道/指令重新排序问题。你永远不应该假设 volatile 会为你做这件事,除非编译器文档明确声明它会这样做。

【讨论】:

  • 我没有使用 volatile 变量作为原子变量、内存屏障或线程同步。
【解决方案3】:

使用volatile,您只能强制在使用变量值时重新读取变量。它不能保证在您的架构的不同级别上存在的不同值/表示是一致的。

要获得这样的保证,您需要 C11 和 C++1 中关于原子访问和内存屏障的新实用程序。许多编译器已经在扩展方面实现了这些。例如,gcc 系列(clang、icc 等)具有以前缀 __sync 开头的内置函数来实现这些。

【讨论】:

  • 我认为使用 __sync 可以确保原子操作,但不会阻止竞争条件。
  • 确保原子避免竞争条件,这正是它们的定义。但它们也保证数据的一致性。
  • 对不起,我应该说原子不能确保线程之间的正确同步。
  • 硬件保证缓存的一致性(除了存储缓冲区)。编译器为atomic<int> 发出的asm 不会做任何事情来确保其他线程可以看到您的存储。这总是发生。不过,seq-cst 存储将使当前线程等待直到发生这种情况,然后再进行其他加载/存储。
【解决方案4】:

Volatile 关键字只保证编译器不会对这个变量使用寄存器。因此,对这个变量的每次访问都会去读取内存位置。现在,我假设您在架构中的多个处理器之间具有缓存一致性。因此,如果一个处理器写入而另一个处理器读取它,那么它在正常情况下应该是可见的。但是,您应该考虑极端情况。假设变量在一个处理器内核的管道中,而另一个处理器试图读取它并假设它已被写入,那么就会出现问题。所以本质上,共享变量应该被锁保护,或者应该通过正确使用屏障机制来保护。

【讨论】:

  • 另外,我想知道在编译时启用了一些优化级别,如果编译器有机会删除了这个语句?这只是一个想法。查看发生了什么的方法之一是使用一些实用程序转储汇编代码。
【解决方案5】:

volatile 的语义是实现定义的。如果编译器知道在执行某些代码时中断将被禁用,并且知道在目标平台上除了中断处理程序之外没有其他方法可以观察到某些存储上的操作,它可以注册缓存@987654323此类存储中的 @ 限定变量就像它可以缓存普通变量一样,只要它记录了此类行为。

请注意,行为的哪些方面被视为“可观察的”,可能在某种程度上由实现定义。如果一个实现证明它不打算在使用主 RAM 访问来触发所需的外部可见操作的硬件上使用,那么在该实现上对主 RAM 的访问将不是“可观察的”。如果没有人关心是否实际看到任何此类访问,则该实现将与能够物理观察此类访问的硬件兼容。但是,如果需要此类访问,就像这些访问被视为“可观察的”时那样,编译器不会声明兼容性,因此不会对任何事情做出任何承诺。

【讨论】:

  • 这是真的,但所有主流编译器都选择让volatile 代表您所期望的,并为每个volatile 访问真正加载或存储。因此,即使您使用调试器或在模拟器中单步进行观察,一切都是正确的。这意味着volatile 确实可以作为一个滚动你自己的atomicmemory_order_relaxed,用于足够窄的类型以自然地成为原子。 (当然,一般来说它是 UB,但您可能会争辩说,实现对 volatile 的定义足够强烈。
  • 相关:MCU programming - C++ O2 optimization breaks while loop) 详细介绍了 volatileatomic 的中断处理程序。
  • @PeterCordes:即使在单核平台上,它们也不能很好地合成互斥锁,除非应该由互斥锁保护的所有东西都符合条件volatile--本质上是一个要求“优化的” C 语言所独有的,它否定了优化的好处。
  • 这就是为什么我说它们作为 relaxed 原子工作,因为它们不能提供获取或释放语义。非易失性对象。 (当然,对于特定于实现的东西,例如asm("" ::: "memory") 编译器屏障和/或硬件屏障,您也可以像 Linux 内核那样滚动自己的内存排序。)但我的主要观点是,尽管标准允许这样做,我认为没有任何实现尝试做你建议的任何事情。
  • @PeterCordes:据我所知,icc 会将volatile readvolatile write 的组合视为发布和获取,尽管两者都不会孤立地工作。 至少在编译器重新排序方面作为获取/释放障碍的能力在系统编程中一直是必不可少的,但标准并不要求这样做因为它使没有试图强制要求所有实现都适合系统编程,而是将其作为实现质量问题。现有代码的大量语料库需要...
【解决方案6】:

对于 C++:

据我所知,编译器从不优化声明为 volatile 的变量。

你的前提是错误的。 volatile 是对编译器的提示,实际上并不保证任何事情。编译器可以选择阻止对 volatile 变量的某些优化,但仅此而已。

volatile 不是锁,不要尝试这样使用它。

7.1.5.1

7) [ 注意:volatile 是对实现的提示,要避免 涉及对象的积极优化,因为 对象可能会通过实现无法检测到的方式进行更改。 详细语义见 1.9。一般来说,volatile的语义 旨在在 C++ 中与在 C 中相同。—尾注]

【讨论】:

  • 实际上,从编译器的角度来看,volatile 的要求相当高。它不像registerinline,编译器可以随意忽略它们。对 volatile 对象的精确访问是符合标准的实现的最低要求之一:如果编译器仅将其视为提示,则该实现是不符合标准的。 (参见 C 标准的 5.1.2.3,我相信它与 C++ 类似。)您的结论是正确的,但不是因为您给出的原因。
  • @hvd 正在寻找报价,但我相信你错了。
  • volatile 并不意味着写入是原子的。这仅适用于 volatile bool - 因此在您读取之前来自另一个线程的写入可能尚未完成
  • 我不了解 C++,但在 C 中不允许编译器优化 volatile。 C11 5.1.2.3/2"Accessing a volatile object, ... are all side effects"。 5.1.2.3/4 "An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object)." 5.1.2.3/6 "The least requirements on a conforming implementation are: — Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine."
  • 这个答案基本正确,volatile 不代表“不优化”。事实上,我认为“不要优化这个变量”甚至没有明确的含义。但是volatile 的语义并不是单独 避免优化的提示,所以volatile 不能保证什么都没有。访问volatile 对象是可观察到的行为。由于 hvd 提到它进行比较,C++ 编译器也不能随意忽略 inlineregister:除了作为优化提示的次要角色之外,它们都有定义的含义。 C 编译器可以忽略register,也可以忽略restrict
【解决方案7】:

Volatile 只影响它前面的变量。在您的示例中,这里是一个指针。您的代码:volatile long array[8],指向数组第一个元素的指针是 volatile,而不是它的内容。 (任何种类的物体都一样)

你可以像在 How do I declare an array created using malloc to be volatile in c++

【讨论】:

    【解决方案8】:

    volatile 关键字没有与 C++ 中的并发完全!它用于阻止编译器使用先前的值,即编译器将生成访问代码中每次访问volatile 值的代码。主要目的是诸如内存映射 I/O 之类的东西。但是,volatile 的使用对 CPU 在读取正常内存时的行为有没有影响:如果 CPU 没有理由相信内存中的值发生了变化,例如,因为没有同步指令,它可以只使用其缓存中的值。要在线程之间进行通信,您需要一些同步,例如 std::atomic<T>、锁定 std::mutex 等。

    【讨论】:

    • 它还可以防止编译器优化掉你“没有使用”的变量。
    • 是的,volatile 与线程无关。但是构成易失性“访问”的内容是实现定义的。这不是 1:1 的代码。
    • 缓存是一致的,因此(以存储缓冲区为模)所有内核共享相同的内存视图。线程不能无限期地从volatile 重新读取相同的陈旧值。 C++ 可能会在需要显式刷新才能使数据全局可见的系统上实现,但这会很昂贵。这个问题被标记为 x86,但这也适用于弱序 ISA,如 PowerPC 和 ARM。
    • 如果你需要让一个线程等到一个存储/加载在它做其他事情之前变得全局可见,你只需要内存屏障; asm 存储已经尽可能快地成为全局可见的,从存储缓冲区提交到 L1d。而volatile 表示编译器会在您期望的时候发出 asm 以实际存储。
    【解决方案9】:

    通过 volatile 左值的 C++ 访问和对 volatile 对象的 C 访问是“抽象的”“可观察的”——尽管 in practice C 的行为符合 C++ 标准而不是 C 标准。非正式地,volatile 声明告诉 每个 线程该值可能会以某种方式改变,而不管 any 线程中的文本如何。在线程标准下,除了在同步关键开始时同步函数调用的共享变量之外,没有任何另一个线程写入导致对象更改的概念,无论是否易失,是否共享地区。 volatile 与线程共享对象无关

    如果您的代码未正确同步您正在谈论的线程,则您的一个线程读取另一个线程所写的内容具有未定义的行为。所以编译器可以生成它想要的任何代码。如果您的代码已正确同步,则其他线程的写入仅发生在线程同步调用中;你不需要volatile

    PS

    标准说“什么构成对对象的访问, 具有 volatile 限定的类型是实现定义的。”因此,您不能只假设对 volatile 左值的每次取消引用都有读取访问权限,或者通过一个赋值对每个赋值都有写入访问权限。

    此外,(“抽象”)“可观察”volatile 访问“实际上”如何体现是由实现定义的。因此,编译器可能不会为与定义的抽象访问对应的硬件访问生成代码。例如,可能只有具有静态存储持续时间的对象和使用特定标志编译以链接到特殊硬件位置的外部链接可以从程序文本外部更改,从而忽略其他对象的volatile

    【讨论】:

      【解决方案10】:

      但是,在某些情况下,我注意到即使我修改了 来自线程的元素,读取它的线程不会注意到 改变。它继续读取相同的旧值,就好像编译器有 缓存在某处。

      这不是因为编译器将它缓存在某个地方,而是因为读取线程从其 CPU 内核的缓存中读取,这可能与写入线程的缓存不同。为确保跨 CPU 内核的值更改传播,您需要使用适当的内存栅栏,而在 C++ 中,您既不能也不需要使用 volatile。

      【讨论】:

      • 但是在像x86这样具有主动缓存一致性的处理器中,在这种情况下应该更新内核的缓存,即每当内核A写入内存X时,如果内核B尝试从X,X对应的缓存会更新。
      • @pythonic:是的,x86 与所有普通 CPU(ARM / PowerPC / MIPS / SPARC / ...)一样,具有一致的缓存。如果您使用volatile,其他核心将很快(在微秒内:If I don't use fences, how long could it take a core to see another core's writes?)注意到来自另一个核心的存储。您只需要在阅读器中订购加载/存储的障碍,或者让编写器在执行其他操作之前等待存储变得全局可见。
      • @pythonic:如果你描述的情况持续了很长时间,那么你做错了什么或者描述错了。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多