在C++中理解'volatile'关键字

3
我正在尝试理解C++中volatile关键字的工作原理。
我查看了What kinds of optimizations does 'volatile' prevent in C++?。从被接受的答案来看,volatile禁用了两种优化:
  1. 防止编译器将值缓存到寄存器中。
  2. 从程序的角度看,优化掉对该值的访问。
我在https://en.cppreference.com/w/cpp/language/as_if上找到了类似的信息。

对volatile对象的访问(读写)严格遵循它们出现的表达式的语义。特别地,它们不会与同一线程上的其他volatile访问重新排序。

我编写了一个简单的C++程序,将数组中所有值相加,以比较普通的int和volatile int的行为。请注意,部分总和不是volatile的。
数组由未经过资格认证的int组成。
int foo(const std::array<int, 4>& input)
{
    auto sum = 0xD;
    for (auto element : input)
    {
        sum += element;
    }
    return sum;
}

数组由易失的 int 组成

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

当我查看生成的汇编代码时,只有在普通的int情况下才会使用SSE寄存器。据我所知,使用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

1
我发现volatile唯一的用例是在处理内存映射I/O和多进程共享内存时。 (我并不声称我的列表是详尽的,只是我遇到的两种情况。) - Eljay
我遇到了一些使用volatile(用于共享内存)的代码,我完全感到困惑。这就是我想更好地理解这个关键字的原因。 - Empty Space
2
“volatile”一直被历史性地误解和滥用。在未来的标准版本中,大多数使用“volatile”的情况可能会被弃用:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1152r0.html,https://embeddedartistry.com/newsletter-archive/2019/3/4/march-2019-deprecating-volatile - bolov
4
这里涉及到内存映射 I/O,读取特定地址的操作会对硬件产生副作用。因此,SIMD 优化不再安全,现在只有一个地址进行一次读取,而不是在4个不同的地址上进行4次读取。 - Hans Passant
当SIMD读取请求通过总线发送时,它不会被分成4个不同的地址吗? - Empty Space
显示剩余9条评论
1个回答

6

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 的编译器,告诉它们不应该对任何之前的对象操作和后续操作之间的相互作用做出任何假设。在其他语言(如 C#)中也遵循了 volatile 限定符。

不幸的是,无论是 clang 还是 gcc 都没有包括任何选项以这种方式处理 volatile,而是要求程序员使用特定于编译器的内部函数来产生 Microsoft 可以仅使用标准关键字 volatile 实现的语义。根据标准的作者,“volatile 对象也是多个进程共享的变量的适当模型。”(请参见http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf第76页第25-26行)


3
易变性在诸如多次从映射到IO的同一地址读取数据等方面是必不可少的。IO设备可能被设置为在每次读取时时钟输入连续的数据块,如果没有易变性,程序可能会假定从同一地址读取未被程序修改的数据将不会更改数值。易变性不应用于与多线程相关但语义不完全相同的情况。 - doug
1
@doug:微软的编译器早于 C 和 C++ 标准,标准出台后也没有表明对于类似平台的实现不应该继续像标准之前那样运行。 - supercat
2
@supercat,是的,那不是C++11或更高标准。它是早期的C标准,那时我写了很多代码。现代的C++必须解决由于缓存层和多个CPU而导致的问题,这些问题在早期的C和C++标准中仅使用volatile是不足够的,会影响效率。在我看来这是好事。另一方面,我从来没有遇到过在C ++ 11或更高版本以及多核硬件上出现竞争条件问题的代码。仅仅使用volatile是不够的,至少默认的Microsoft编译器不能保证性能。 - doug
1
在许多情况下,编译器不需要volatile作为一种指示,表明它们应该在处理使用I/O寄存器的代码时保持谨慎,因为这样的代码通常会访问刚从整数转换而来的指针。由于程序很少从整数形成指针,除非涉及做一些“奇怪”的事情,在不推断此类指针来源的编译器自然可以妥善处理I/O寄存器,即使没有volatile - supercat
1
@TrickorTreat: C89标准的作者们认为不必要明确规定用于各种目的的实现应该提供适合这些目的的“volatile”语义,因为这似乎是显而易见的。为后来的标准添加更多细节需要共识,然而编译器编写者未能接受C89委员会认为可以不言而喻的事情,导致共识无法形成。 - supercat
显示剩余15条评论

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