为什么易变的局部变量与volatile参数的优化不同,为什么优化器会从后者生成无操作循环?

und*_*e_d 8 c++ optimization g++ volatile pass-by-value

背景

这是受到这个问题/答案以及随后在评论中讨论的启发:"易变"这个定义是不稳定的,还是GCC有一些标准的合规性问题?.基于其他人以及我对应该发生的事情的解释,如评论中所讨论的,我已将其提交给GCC Bugzilla:https://gcc.gnu.org/bugzilla/show_bug.cgi id = 71793 其他相关回复仍然存在欢迎.

此外,该线程引发了这个问题:通过易失性引用/指针访问声明的非易失性对象是否会在所述访问时赋予易失性规则?

介绍

我知道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);
}
Run Code Online (Sandbox Code Playgroud)

产量

g++来自gcc (Debian 4.9.2-10) 4.9.2(Debian stableaka Jessie)的命令行g++ -std=c++14 -O3 -S test.cpp生成以下ASM main().版本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
Run Code Online (Sandbox Code Playgroud)

分析

所有3个函数都是内联的,并且volatile由于相当明显的原因,它们都在堆栈中分配局部变量.但这是他们共享的唯一事情......

  • f()确保从x每次迭代读取,可能是由于它volatile- 但只是将结果转储到edx,可能是因为目标y未被声明volatile且永远不会被读取,这意味着对它的更改可以在as-if规则下固定.好的,有道理.

    • 嗯,我的意思是...... 有点儿.喜欢,不是真的,因为volatile它实际上是硬件寄存器,显然本地值不能是其中之一 - 否则不能以某种volatile方式修改,除非它的地址被传递出来......但事实并非如此.看,没有太多意义可以超越volatile当地的价值观.但是C++允许我们声明它们并试图用它们做些什么.所以,一如既往地困惑,我们绊倒了.
  • g():什么 通过将volatile源移动到pass-by-value参数(仍然只是另一个局部变量),GCC以某种方式决定它不是或更少 volatile,因此它不需要每次迭代都读取它...但它仍然执行循环,尽管它的身体现在什么都不做.

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

    • 由于上面提到的原因,仅此案例对我来说实际上是有意义的f().详细说明:Imagine x是指硬件寄存器,每次读取都有副作用.你不想跳过任何这些.

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

问题

  • 为什么在体内声明的局部值会产生与按值参数不同的结果,前者的读取数据会被优化掉?两者都被宣布volatile.没有地址传出 - 并且没有static地址,排除任何内联ASM POKEry - 所以它们永远不能被修改掉.编译器可以看到每个都是常量,永远不需要重新读取,并且volatile不是真的 -
    • 那么(A)是否允许在这种限制下被省略?(如果没有宣布他们的话volatile) -
    • (B)为什么只有一个被剔除?一些volatile局部变量是否volatile比其他变量更多?
  • 暂时搁置这种不一致性:在优化读取之后,为什么编译器仍会生成循环?它什么都不做!为什么优化器不会忽略- 如果没有编码循环?

由于优化分析的顺序等,这是一个奇怪的角落案例吗?由于代码是一个愚蠢的思想实验,我不会因此而惩罚海湾合作委员会,但肯定知道这一点很好.(或者是g()这些年来人们梦寐以求的手动定时循环?)如果我们得出结论,对此任何内容都没有标准,我将把它移到他们的Bugzilla只是为了他们的信息.

当然,从实际的角度来看,更重要的问题,虽然我不希望这会掩盖编译器极客的可能性......根据标准,哪些(如果有的话)是明确的/正确的?

avd*_*ten 2

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

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

  • 编译器无法在内存中分配“x”,因为 ABI 没有特殊情况的易失性参数(因为它们没有多大意义),而只是像非易失性参数一样将它们传递到寄存器中。在内存中分配“x”会破坏 ABI 的函数调用顺序。请注意,编译器将被允许将“x”复制到不同的位置(但为什么对此位置的读/写应该保持有序/不变?),但它肯定不能在不破坏 ABI 的情况下接受内存中的“x”参数。 (2认同)