现在"{static const char a[]={...}"和"{const char a[]={...}"之间有什么区别?

27

请看这段微小的C代码C++代码,它在godbolt上......

void b( char const *c);

void a(void)
{
   char const z[] = {0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf, 0xa};

   b(z);
}

void c(void)
{
   static char const z[] = {0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf, 0xa};

   b(z);
}
早期版本的gcc将a()和c()编译为两条指令:加载z的地址并跳转到b。
我尝试过的所有现代编译器都会将a()“悲观化”为“创建堆栈帧,将z复制到堆栈上,调用b,拆除堆栈帧,但将c()保留为两条指令的简单版本。”
实际上没有任何变化,在这种情况下现代编译器在实践中变得更慢了.....
有人知道为什么吗?

1
注意:c()b(z)传递了一个指向数组的指针,该指针在b()完成后仍然有效。但是a()b(z)不是这样。有趣的是,将restrict添加到void b(char const * restrict c);中是否会改变情况? - chux - Reinstate Monica
不是。https://gcc.godbolt.org/z/cB5-w7 - John Carter
1
static char const z[],表示 z 是静态变量。静态变量必须位于数据段中。因此编译器会使用两条指令。对于 char const z[],z 是常量变量,我认为编译器可以将其放在数据段或堆栈中。我认为这只是编译器的选择,但并不确定。如果使用char z[],那么编译器将把z放在堆栈中,然后函数a将有更多的指令。 - randomeval
有趣的是,gcc曾经选择将其留在数据部分,以使其更快...然后改变了这种做法,使其更慢。此外,我在godbolt上尝试的所有其他近期编译器也使用了较慢的版本。这表明存在一种很好的理由...可惜,我无法想出是什么原因。 - John Carter
我看到你使用了编译选项“-Os”。我尝试使用编译器选项“-std=c99”来使用clang编译器。不同版本的clang指令是不同的,但它们似乎都将数组z放在数据段中。clang with std - randomeval
-std=c99仅要求编译器使用c99标准。-Os优化代码大小。它与-O2相同,但删除了任何使输出膨胀的优化。https://gcc.godbolt.org/z/s4xT3K - John Carter
2个回答

17

C++有以下 规则

除非对象是位域或零大小的子对象,否则该对象的地址为其占用的第一个字节的地址。不是位域且具有重叠生命周期的两个对象,如果一个嵌套在另一个内部,或者至少一个是零大小的子对象且它们具有不同的类型,则其可以具有相同的地址;否则,它们具有不同的地址并占用不相交的存储字节

现在,请看这段代码:

#include <stdio.h>

void c();

void b(const char *a) {
    static const char *p = 0;

    if (!p) {
        p = a;
        c();
    } else {
        if (a==p) {
            printf("problem!\n");
        }
    }
}

void c() {
    const char a[] = { 0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf };

    b(a);
}

int main() {
    c();
}

在这里,c被递归调用了一次,所以根据规则,数组a在每个递归级别中应该具有不同的地址。 b在第一次调用时将a存储在其中,在第二次调用时,它检查是否相同。 使用符合规范的编译器,它不应该打印“问题!”。 但实际上,使用旧的编译器(GCC 4.1,clang 6.0),它会打印“问题!”,因此这些编译器违反了标准。

编译器只允许在可以证明此更改不是可观察到的情况下将a设为静态:

按照“as-if”规则,如果程序无法观察到区别,则实现允许将两个对象存储在相同的机器地址或根本不存储对象


我只能点赞一次,但这基本上是我想到的相同示例“b”函数,而且解释得很好,平衡了易懂和技术正确性。 - aschepler
1
C标准对此并没有明确说明,但我认为6.2.4/6(非VLA)和6.2.4/7(VLA)暗示了同样的要求:“如果递归进入[block/scope],则每次都会创建对象的新实例。” - aschepler
好的,你可能需要提到一个例外:所有字符串字面值都可以共享空间。 - Deduplicator
@Deduplicator 任何给定的字符串字面量都是一个静态对象。具有相同值的多个字符串字面量是否是不同的对象是未指定的。 - Caleth
1
@Caleth 这意味着,如果一个字符串字面值是另一个字符串字面值的后缀,它们不能共享空间。但实际上并没有这样的限制。 - Deduplicator

4
我希望的答案是编译器会按照您在代码中指定的内容执行 - 必须有一个本地函数数组,其存储不与其他线程共享,并且该数组将传递给其他函数。以前,编译器可以使用“as-if”规则将其删除并将其放置在其他位置,因为语言模型中不存在线程,但是由于现在存在线程,它必须确保不会意外地与他人造成虚假共享。它可能已经使其成为线程局部变量,但这比仅限于函数本地还要糟糕。
请注意,GCC从未进行过此优化,但Clang在6.0.0之后停止了这样做。甚至使用了这种优化可能是Clang的一个错误。

2
但是数组是“const”的,因此不可能存在竞争条件。 - Quentin

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