【问题标题】:What kinds of optimizations does 'volatile' prevent in C++?'volatile' 在 C++ 中阻止了哪些优化?
【发布时间】:2011-04-05 23:55:10
【问题描述】:

我正在查找关键字volatile 及其用途,得到的答案差不多是:

用于防止编译器优化掉代码。

有一些示例,例如在轮询内存映射硬件时:如果没有volatile,轮询循环将被删除,因为编译器可能会认识到条件值永远不会改变。但是由于只有一个或两个示例,这让我开始思考:是否还有其他情况需要使用volatile 来避免不必要的优化?条件变量是唯一需要volatile 的地方吗?

我认为优化是特定于编译器的,因此未在 C++ 规范中指定。这是否意味着我们必须凭直觉说 嗯,如果我不将该变量声明为 volatile 或者是否有任何明确的规则,我怀疑我的编译器会取消这个由?

【问题讨论】:

  • “不要使用volatile”是可接受的规则吗?因为它是一个相当不错的。并不是说volatile 永远不会有用。只是,一般来说,如果您不确定是否需要它,您可能不需要。
  • @Dennis:+1,并在我的回答中这样评论。
  • @DennisZickefoose "“不要使用volatile”是一个可接受的规则吗?" 我会说:除非有一些官方标准,否则不要使用volatile,参考文本或文档告诉您这样做。
  • 条件变量不需要 volatile。条件变量具有库支持(Win32、pthread、boost、std...)并使用互斥锁完全锁定。

标签: c++ optimization volatile


【解决方案1】:

基本上,volatile 宣布某个值可能会在您的程序背后发生变化。这可以防止编译器缓存该值(在 CPU 寄存器中),并在您的程序的 POV 中似乎不必要时优化对该值的访问。

应该触发volatile 的使用是当值发生变化时,尽管您的程序没有写入它,并且没有其他内存屏障(如用于多线程程序的互斥锁)存在。

【讨论】:

  • 虽然我认为这个问题的几乎所有答案都或多或少有用(我想将它们全部捆绑在一起接受,呵呵),我想说这个总结一下.
  • 所以在这个模型下,对volatile 的两次连续写入可以折叠成一个,对吧?因为您的描述仅涵盖影响读取的优化。
【解决方案2】:

Volatile 不会尝试将数据保存到 cpu 寄存器(比内存快 100 倍)。每次使用时都要从内存中读取。

【讨论】:

  • volatile 不会从 L1 缓存中排除该值,这只需几个周期即可访问。但是,它与其他机制相关联。设备寄存器总是易失性的,而且通常比 DRAM 还要慢。
  • @Potatoswatter 一级缓存不是由硬件控制的吗?我不知道软件会影响那里的任何东西。
  • @Potatoswatter:虽然确实不需要一个 volatile 变量来使其一直到真实内存(可能取决于架构),但事实是它可以有一个影响远大于几个周期。如果变量与其他 CPU 使用的任何变量位于同一缓存行中,则对 volatile 变量的每次操作都会触发与其他 CPU 的缓存同步,这对volatile 本身和非同一缓存行中的易失性变量。
  • @Byron:硬件配置设置由软件设置。操作系统可以调用 MMU 并关闭给定页面的缓存。甚至可能有一个用户空间工具可以让任何程序这样做。
  • @David:是的,但这也适用于没有经过优化的非易失性变量。
【解决方案3】:

条件变量在需要volatile 的地方不是;严格来说,它仅在设备驱动程序中需要。

volatile 保证对象的读写不会被优化掉,或者相对于另一个volatile 重新排序。如果您正忙于循环另一个线程修改的变量,则应将其声明为volatile。但是,您不应该忙循环。因为该语言并不是真正为多线程设计的,所以它没有得到很好的支持。例如,编译器可能会将对 non-volatile 变量的写入从循环之后移动到循环之前,从而违反了锁。 (对于无限自旋循环,这可能只发生在 C++0x 下。)

当您调用线程库函数时,它充当内存栅栏,编译器将假定所有值都已更改 — 基本上所有内容都是易失的。这是由任何线程库指定或默认实现的,以保持车轮平稳转动。

C++0x 可能没有这个缺点,因为它引入了正式的多线程语义。我不太熟悉这些变化,但为了向后兼容,它不需要声明任何以前没有的 volatile。

【讨论】:

  • "局部变量根本不允许是 volatile 的,虽然有些编译器可能支持它并且你可以选择通过 volatile * 独占访问局部变量" -> 我已经看到局部 volatile 变量,等待中嵌入式系统的循环和很快。您能否为此提供标准参考?另请注意,如果您有一个int 对象,但仅通过int volatile& 访问它(或通过取消引用int volatile*),这不被视为易失性读/写,因此编译器可能会将其优化掉.
  • 请注意,著名的 C++ and the Perils of Double-Checked Locking 到处都使用对 int volatile& 的强制转换,并使用 C++03 标准的一个特定引用,即“抽象机器的可观察行为是它的对易失性数据的读取和写入序列以及对库 I/O 函数的调用”。
  • 它解释说,实际上 访问路径 易失性足以使访问可观察到,但 C++03 也表示“对一致实现的最低要求是: [..] 在序列点,易失性对象是稳定的,因为之前的评估已经完成,而后续的评估还没有发生。”。请注意,此文本很清楚,只有对 volatile 对象 的访问而不是 访问路径 单独确定访问是否是可观察的行为。
  • 最后,C++0x 是最清楚的,只是说“对 volatile 对象的访问严格按照抽象机的规则进行评估。”。感谢DR #612,它不再尝试多次定义可观察行为。所以试图通过使用可怕的演员表来添加可观察的行为实际上是行不通的。
  • @Johannes:这取决于您如何定义“最后存储的值”。根据定义,某物包含的值是最后存储到其中的值……反之亦然。因此,如果允许被调用函数将值存储到调用者的框架中(大概是这样),那么您的示例是有效的(有或没有volatile)。但是,应该允许其他线程、操作系统或外部设备修改对象,这有点牵强——这会使段落变得毫无意义。 (也许,根据其循环逻辑,它是。)无论如何,关键是它适用于所有本地人,无论volatile 资格如何。
【解决方案4】:

C++ 程序的可观察行为由对 volatile 变量的读写以及对输入/输出函数的任何调用决定。

这意味着所有对 volatile 变量的读取和写入必须按照它们在代码中出现的顺序发生,并且它们必须发生。 (如果编译器违反了这些规则之一,那就是违反了 as-if 规则。)

就是这样。当您需要指示读取或写入变量将被视为可观察的效果时使用它。 (注意,"C++ and the Perils of Double-Checked Locking" article 涉及到这一点。)


所以要回答标题问题,它会阻止任何可能重新排序 volatile 变量的评估的优化相对于其他 volatile 变量

这意味着编译器会发生变化:

int x = 2;
volatile int y = 5;
x = 5;
y = 7;

int x = 5;
volatile int y = 5;
y = 7;

很好,因为x 的值不是可观察行为的一部分(它不是易变的)。将赋值从 5 更改为 7 是不行的,因为写入 5 是可观察到的效果。

【讨论】:

  • "volatile y" C++ 中不存在隐含的int 规则。
  • @curiousguy:确实,一个老错字。
【解决方案5】:

通常编译器假定程序是单线程的,因此它完全了解变量值发生了什么。然后,智能编译器可以证明该程序可以转换为另一个具有等效语义但性能更好的程序。例如

x = y+y+y+y+y;

可以转化为

x = y*5;

但是,如果可以在线程外更改变量,则编译器无法通过简单地检查这段代码来完全了解正在发生的事情。它不能再像上面那样进行优化。 (编辑:在这种情况下可能可以;我们需要更复杂的示例

默认情况下,为了性能优化,假定单线程访问。这个假设通常是正确的。除非程序员使用 volatile 关键字明确指示。

【讨论】:

  • 实际上,我相信常量折叠在volatile 项目上仍然有效,这基本上就是您在此处显示的内容。
  • 我不是 c++ 专家。在 java 中,volatile y 必须被获取 5 次。
  • @Billy:我同意答案有点不清楚,但要澄清一下:如果y 不稳定,那么将x = y + y 更改为x = 2 * y好的.但是将y = 2 + 2 更改为y = 4 很好。
  • @Billy ONeal:如果代码读取 volatile 变量五次,则该变量必须精确读取五次。一条语句:“a=(b && volatilevar);”不能写成“a = (volatilevar & -!!b);”即使后一种形式会更快(因为没有分支),因为即使 b 为假,后一种形式也会读取 volatilevar。
  • volatile 关键字未添加到语言中以考虑多线程。即使含义有些相似,其意图也是通过内存地址访问硬件组件。 volatile 意味着变量的值可以在程序之外改变——不仅仅是线程,而是整个程序——甚至读取可能会在程序所做的事情之外产生副作用——即想象一个在每次读取时递增的硬件计数器。
【解决方案6】:

除非您在嵌入式系统上,或者您正在编写使用内存映射作为通信手段的硬件驱动程序,否则您应该永远使用volatile

考虑:

int main()
{
    volatile int SomeHardwareMemory; //This is a platform specific INT location. 
    for(int idx=0; idx < 56; ++idx)
    {
        printf("%d", SomeHardwareMemory);
    }
}

必须生成如下代码:

loadIntoRegister3 56
loadIntoRegister2 "%d"
loopTop:
loadIntoRegister1 <<SOMEHARDWAREMEMORY>
pushRegister2
pushRegister1
call printf
decrementRegister3
ifRegister3LessThan 56 goto loopTop

如果没有volatile,它可能是:

loadIntoRegister3 56
loadIntoRegister2 "%d"
loadIntoRegister1 <<SOMEHARDWAREMEMORY>
loopTop:
pushRegister2
pushRegister1
call printf
decrementRegister3
ifRegister3LessThan 56 goto loopTop

关于volatile 的假设是变量的内存位置可能会改变。每次使用变量时,您都在强制编译器加载实际值从内存;然后告诉编译器不允许在寄存器中重用该值。

【讨论】:

    【解决方案7】:

    请记住,“好像规则”意味着编译器可以而且应该做它想做的任何事情,只要从程序外部看到的行为作为一个整体是相同的。特别是,虽然变量在概念上命名了内存中的一个区域,但没有理由让它实际上应该在内存中。

    可以在一个寄存器中:

    它的值可以被计算掉,例如在:

    int x = 2;
    int y = x + 7;
    return y + 1;
    

    根本不需要xy,但可以替换为:

    return 10;
    

    另一个例子是,任何不影响外部状态的代码都可以被完全删除。例如。如果您将敏感数据归零,编译器会将其视为浪费的练习(“为什么要写入不会被读取的内容?”)并将其删除。 volatile 可以用来阻止这种情况的发生。

    volatile 可以被认为是“这个变量的状态必须被认为是外部可见状态的一部分,而不是被弄乱”。不允许使用它而不是按照源代码进行优化。

    (注意 C#。我最近在 volatile 上看到的很多内容表明人们正在阅读 C++ volatile 并将其应用于 C#,并在 C# 中阅读它并将其应用于 C++。虽然, volatile 在两者之间的行为如此不同,以至于认为它们相关没有用)。

    【讨论】:

    • +1,重要的是要考虑内存模型和程序的可见状态。如果编译器可以确定变量不会影响可见行为,编译器甚至可以丢弃 volatile 限定符。考虑一个在不调用任何其他函数的函数中声明为 volatile 的自动变量。编译器可以确定变量不能在线程外轮询,并可以决定应用它希望的任何优化。
    • @David:根据定义,对 volatile 的读取和写入是 C++ 程序可见行为的一部分。优化器在“as-if”规则下工作,如果它们保持可见行为不变,则允许转换。因此,优化可能不会消除对 volatile 对象的读写。
    • @MSalters,David 是对的,如果自动变量是 volatile 但没有将其地址传递给非自动 volatile 指针,或者可能参与长跳转或以其他方式在外部访问“正常”的功能访问,那么就无法从外部观察它,因为外部的任何东西都无法知道要观察什么。在这种情况下,可以确定它并不是真正的波动性,并且它的波动性被忽略了。
    • @MSalters:我没能找到引用,但我很确定这是 Herb Sutter 在过去几个月里的一句话,他提到编译器可以证明一个 volatile 变量可以证明在另一个上下文中不可见——即地址在堆栈中,可以证明它没有传递给其他函数,因此不能从其他上下文中查询——可以证明它不是程序可见行为的一部分,并且符合标准的编译器可以丢弃volatile 限定符。
    • @MSalters:... 但如果没有适当的引用,请加一点盐,因为我一开始可能误解了评论。
    【解决方案8】:

    考虑 volatile 变量的一种方法是把它想象成一个虚拟属性。写入甚至读取可能会做编译器无法知道的事情。写入/读取 volatile 变量的实际生成代码只是内存写入或读取(*),但编译器必须将代码视为不透明的;它不能做出任何可能是多余的假设。问题不仅仅在于确保编译后的代码注意到某些东西导致变量发生变化。在某些系统上,甚至内存读取也可以“做”一些事情。

    (*) 在某些编译器上,volatile 变量可以作为不同的操作添加、减去、递增、递减等。编译器进行编译可能很有用:

    易失性++;

    作为

    公司 [_volatilevar]

    因为后一种形式在许多微处理器上可能是原子的(尽管不是在现代多核 PC 上)。然而,重要的是要注意,如果语句是:

    volatilevar2 = (volatilevar1++);

    正确的代码不是:

    mov ax,[_volatilevar1] ;读一次 公司 [_volatilevar] ;再读一遍(哎呀) mov [_volatilevar2],ax

    也没有

    移动斧头,[_volatilevar1] mov [_volatilevar2],ax ;写错顺序 公司斧头 mov [_volatilevar1],ax

    而是

    移动斧头,[_volatilevar1] mov bx,ax 公司斧头 mov [_volatilevar1],ax mov [_volatilevar2],bx

    以不同的方式编写源代码可以生成更高效(并且可能更安全)的代码。如果 'volatilevar1' 不介意被读取两次并且 'volatilevar2' 不介意在 volatilevar1 之前写入,则将语句拆分为

    易失性变量2 = 易失性变量1; 易失性1++;

    将允许更快且可能更安全的代码。

    【讨论】:

    • 很抱歉,我找不到您关于 volatilevar2 = (volatilevar1++); 的订单索赔的理由。只有一个序列点,位于;。因此无法保证写入发生的顺序。
    • @MSalters:您在这一点上可能是对的,在这种情况下,第三种变体是可以接受的。另一方面,使用 inc [_volatilevar1] 的最快版本肯定是不可接受的,尽管在某些情况下它比更长的版本更不容易出现问题。
    猜你喜欢
    • 1970-01-01
    • 2018-07-16
    • 2012-12-05
    • 2019-07-12
    • 1970-01-01
    • 1970-01-01
    • 2010-10-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多