【问题标题】:Why is a volatile local variable optimised differently from a volatile argument, and why does the optimiser generate a no-op loop from the latter?为什么 volatile 局部变量的优化与 volatile 参数不同,为什么优化器会从后者生成无操作循环?
【发布时间】:2016-07-06 22:55:12
【问题描述】:

背景

这是受到此问题/答案以及随后在 cmets 中的讨论的启发:Is the definition of “volatile” this volatile, or is GCC having some standard compliancy problems?。根据其他人和我对应该发生的事情的解释,正如 cmets 中所讨论的,我已将其提交给 GCC Bugzilla:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 其他相关回复仍然欢迎。

此外,该线程已经引起了这个问题:Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?

简介

我知道volatile 不是大多数人认为的那样,实现定义的毒蛇巢。而且我当然不想在任何实际代码中使用以下构造。也就是说,我对这些示例中发生的事情完全感到困惑,所以我非常感谢任何说明。

我的猜测是,这是由于对标准的高度细微的解释,或者(更有可能?)只是使用的优化器的极端情况。无论哪种方式,虽然学术性大于实用性,但我希望这被认为是有价值的分析,尤其是考虑到 volatile 被误解的程度。更多的数据点 - 或者更有可能是反对它的点 - 一定是好的。

输入

鉴于此代码:

#include <cstddef>

void f(void *const p, std::size_t n)
{
    unsigned char *y = static_cast<unsigned char *>(p);
    volatile unsigned char const x = 42;
    // N.B. Yeah, const is weird, but it doesn't change anything

    while (n--) {
        *y++ = x;
    }
}

void g(void *const p, std::size_t n, volatile unsigned char const x)
{
    unsigned char *y = static_cast<unsigned char *>(p);

    while (n--) {
        *y++ = x;
    }
}

void h(void *const p, std::size_t n, volatile unsigned char const &x)
{
    unsigned char *y = static_cast<unsigned char *>(p);

    while (n--) {
        *y++ = x;
    }
}

int main(int, char **)
{
    int y[1000];
    f(&y, sizeof y);
    volatile unsigned char const x{99};
    g(&y, sizeof y, x);
    h(&y, sizeof y, x);
}

输出

g++ 来自 gcc (Debian 4.9.2-10) 4.9.2(Debian stable 又名 Jessie)和命令行 g++ -std=c++14 -O3 -S test.cppmain() 生成以下 ASM。版本Debian 5.4.0-6(当前unstable)产生等效代码,但我只是碰巧先运行旧版本,所以它是:

main:
.LFB3:
    .cfi_startproc

# f()
    movb    $42, -1(%rsp)
    movl    $4000, %eax
    .p2align 4,,10
    .p2align 3
.L21:
    subq    $1, %rax
    movzbl  -1(%rsp), %edx
    jne .L21

# x = 99
    movb    $99, -2(%rsp)
    movzbl  -2(%rsp), %eax

# g()
    movl    $4000, %eax
    .p2align 4,,10
    .p2align 3
.L22:
    subq    $1, %rax
    jne .L22

# h()
    movl    $4000, %eax
    .p2align 4,,10
    .p2align 3
.L23:
    subq    $1, %rax
    movzbl  -2(%rsp), %edx
    jne .L23

# return 0;
    xorl    %eax, %eax
    ret
    .cfi_endproc

分析

所有 3 个函数都是内联的,并且分配 volatile 局部变量的两个函数都在堆栈上这样做,原因很明显。但这是他们唯一分享的东西......

  • f() 确保在每次迭代时从 x 读取,可能是因为它的 volatile - 但只是将结果转储到 edx,可能是因为目标 y 不是'未声明 volatile 并且从未被读取,这意味着可以根据 as-if 规则对其进行更改。好的,有道理。

    • 嗯,我的意思是……有点。就像,不是真的,因为volatile 真的是用于硬件寄存器,显然本地值不能是其中之一 - 并且不能以volatile 方式修改,除非它的地址被传递出去......它不是。看,volatile 本地值没有多大意义。但是 C++ 允许我们声明它们并尝试对它们做一些事情。于是,我们一如既往地迷茫,跌跌撞撞地向前走。
  • g()什么。 通过将 volatile 源移动到传递值参数中,这仍然只是另一个局部变量, GCC 以某种方式决定它不是或 less volatile,因此它不需要每次迭代都读取它......但它仍然执行循环,尽管它的主体现在什么都不做.

  • h():通过将传递的volatile作为pass-by-reference,恢复了与f()相同的有效行为,因此循环读取volatile。 p>

    • 仅这个案例对我来说实际上是有实际意义的,因为上面针对f() 列出的原因。详细说明:想象x 指的是一个硬件寄存器,每次读取都有副作用。您不会想跳过其中任何一个。

添加#define volatile /**/ 会导致main() 成为无操作,正如您所料。因此,当存在时,即使在局部变量 volatile 上也会做一些事情......我只是不知道在g() 的情况下 what。那里到底发生了什么?

问题

  • 为什么在体内声明的局部值会产生与按值参数不同的结果,而前者会让读取被优化掉?两者都被声明为volatile。既没有传递地址 - 也没有 static 地址,排除了任何内联 ASM POKEry - 所以它们永远不能被函数修改。编译器可以看到每个都是常量,不需要重新读取,volatile 只是不正确 -
    • 所以 (A) 是否在这种约束下允许被省略? (表现好像他们没有被宣布volatile)-
    • 和 (B) 为什么只有一个被省略?某些volatile 局部变量比其他volatile 多吗?
  • 暂时搁置这种不一致:优化读取后,为什么编译器仍会生成循环? 它什么都不做!为什么优化器不删除它好像没有循环被编码?

由于优化分析的顺序等原因,这是一个奇怪的极端情况吗?由于代码是一个愚蠢的思想实验,我不会为此责备 GCC,但很高兴知道这一点。 (或者g() 是人们多年来梦寐以求的手动计时循环?)如果我们断定这与标准无关,我会将其移至他们的 Bugzilla 以供参考。

当然,从实际的角度来看,更重要的问题是,尽管我不希望这掩盖编译器极客的潜力...根据标准,如果有的话,哪个是明确定义/正确的?

【问题讨论】:

  • TL;DR - 如果它不改变程序的可观察行为真的重要吗?
  • @CaptainObvlious 对 volatile 变量(甚至是自动变量)的修改被认为是可观察的行为
  • 我会说g 是根据标准的编译器错误
  • @underscore_d 是的,读取也是可观察到的行为
  • @DavidSchwartz 该标准规定,系统必须对与x 对应的内存位置进行读取,每次循环迭代一次。如果系统(无论是编译器,还是 CPU 或其他)将所有这些组合到一个读取中,那将是不合格的。

标签: c++ optimization g++ volatile pass-by-value


【解决方案1】:

对于 f:GCC 消除了非易失性存储(但不是负载,如果源位置是内存映射的硬件寄存器,这可能会产生副作用)。这里真的没有什么令人惊讶的。

对于 g:由于 x86_64 ABI 的参数 xg 在寄存器中分配(即rdx),并且在内存中没有位置。读取通用寄存器不会产生任何可观察到的副作用,因此会消除死读。

【讨论】:

  • 这听起来与 Richard Biener 在我的票证上回复的内容相似 - gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 - 但他对票证的回复和编辑,主要是标签 wrong-code,表明他认为这不行.你?在寄存器中分配g(x) 似乎是ABI 的一个细节——在这种情况下,感谢机械解释——但不允许破坏volatile。看起来编译器应该改变它的行为以在这种情况下正常运行。
  • 嗯,你期望什么行为?编译器无法发出内存读取,因为没有可读取的内存位置。抽象机的读操作在这里真的是空操作。它可以将值复制到内存并从该位置读取,但这只有在x 可以实际转义g 时才有意义。
  • 编译器无法在内存中分配x,因为 ABI 没有特殊情况下的易失性参数(因为它们没有多大意义),只是像非易失性参数一样将它们传递到寄存器中。在内存中分配x 会破坏ABI 的函数调用序列。请注意,编译器将被允许将x 复制到不同的位置(但为什么要对该位置的读/写保持有序/未更改?)但它肯定不能在不破坏 ABI 的情况下接受内存中的x 参数。
  • @avdgrinten:为什么编译器不能在内存中分配 x?调用者不会将值放入内存,但这意味着函数序言代码必须这样做。如果x 是易失性的并且该函数包含一个setjmp,我认为编译器可能不得不将它保存在内存中并将其视为volatile,除非编译器知道没有setjmp 将其地址被占用意外发生。
  • 我同意,如果函数包含setjmp 或让指向本地 volatile 变量的指针转义函数,编译器当然应该将值复制到内存中(而不是省略对此内存位置的访问) .在其他情况下,我对标准的解读是,即使变量没有存储在寄存器中,也可以省略 volatile 读/写:编译器的行为as-if volatile 读/写实际上发生了,因为它可以证明没有人(甚至没有内存映射硬件、信号处理程序或其他异步事件)可以实际观察到访问。
猜你喜欢
  • 2015-03-05
  • 2018-04-25
  • 2017-09-20
  • 2018-11-01
  • 2018-12-30
  • 1970-01-01
  • 2010-09-29
  • 1970-01-01
相关资源
最近更新 更多