首页 > 解决方案 > 理解 C++ 中的 volatile 关键字

问题描述

我试图了解 volatile 关键字在 C++ 中的工作原理。

我看了一下C++ 中的“易失性”防止了哪些优化?. 查看接受的答案,看起来 volatile 禁用了两种优化

  1. 防止编译器将值缓存在寄存器中。
  2. 当您的程序的 POV 似乎不需要访问该值时,优化访问该值。

我在https://en.cppreference.com/w/cpp/language/as_if找到了类似的信息。

对 volatile 对象的访问(读取和写入)严格按照它们出现的表达式的语义进行。特别是,它们不会针对同一线程上的其他易失性访问重新排序。

我编写了一个简单的 C++ 程序,它将数组中的所有值相加,以比较 plain ints 与 volatile ints 的行为。请注意,部分总和不是易变的。

数组由不合格int的 s 组成。

int foo(const std::array<int, 4>& input)
{
    auto sum = 0xD;
    for (auto element : input)
    {
        sum += element;
    }
    return sum;
}

数组由 volatileint组成

int bar(const std::array<volatile int, 4>& input)
{
    auto sum = 0xD;
    for (auto element : input)
    {
        sum += element;
    }
    return sum;
}

当我查看生成的汇编代码时,SSE 寄存器仅用于普通ints 的情况。据我所知,使用 SSE 寄存器的代码既没有优化读取,也没有重新排序它们。循环已展开,因此也没有分支。我可以解释为什么代码生成不同的唯一原因是:易失性读取可以在累积发生之前重新排序吗?显然,sum不是易变的。如果这种重新排序不好,是否有可以说明问题的情况/示例?

使用 clang9 生成的代码

foo(std::array<int, 4ul> const&):                # @foo(std::array<int, 4ul> const&)
        movdqu  (%rdi), %xmm0
        pshufd  $78, %xmm0, %xmm1       # xmm1 = xmm0[2,3,0,1]
        paddd   %xmm0, %xmm1
        pshufd  $229, %xmm1, %xmm0      # xmm0 = xmm1[1,1,2,3]
        paddd   %xmm1, %xmm0
        movd    %xmm0, %eax
        addl    $13, %eax
        retq
bar(std::array<int volatile, 4ul> const&):               # @bar(std::array<int volatile, 4ul> const&)
        movl    (%rdi), %eax
        addl    4(%rdi), %eax
        addl    8(%rdi), %eax
        movl    12(%rdi), %ecx
        leal    (%rcx,%rax), %eax
        addl    $13, %eax
        retq

标签: c++

解决方案


C++ 中的volatile关键字是从 C 继承而来的,在 C++ 中,它旨在作为一个通用的包罗万象的地方,以指示编译器应该允许读取或写入对象可能具有它不知道的副作用的可能性的地方。由于不同平台可能引起的副作用种类会有所不同,因此该标准留下了编译器编写者对如何最好地为客户服务的判断的问题。

微软的 8088/8086 和后来的 x86 编译器几十年来一直被设计为支持使用volatile对象来构建保护“普通”对象的互斥锁的做法。举个简单的例子:如果线程 1 执行以下操作:

ordinaryObject = 23;
volatileFlag = 1;
while(volatileFlag)
  doOtherStuffWhileWaiting();
useValue(ordinaryObject);

并且线程 2 定期执行以下操作:

if (volatileFlag)
{
  ordinaryObject++;
  volatileFlag=0;
}

那么对 的访问volatileFlag将作为对 Microsoft 编译器的警告,他们应该避免假设任何对象上的任何先前操作将如何与以后的操作交互。volatile其他语言(如 C#)中的限定符也遵循此模式。

不幸的是,clang 和 gcc 都没有包含任何以volatile这种方式处理的选项,而是选择要求程序员使用编译器特定的内在函数来产生与 Microsoft 仅使用volatile旨在适用于此类目的的 Standard 关键字即可实现的相同语义[根据标准的作者,“一个volatile对象也是在多个进程之间共享的变量的适当模型。”——参见http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5 .10.pdf第 76 ll。25-26]


推荐阅读