C++ 全局常量数组 : Is it guaranteed to be merged (optimized) into one copy?

标签 c++ c++11

[读者注意:作为对这个问题的降级评论的建议,我添加了这个注意通知:不要假设这个问题的任何部分都是真实的陈述:我提出这个问题,部分原因是我的一些知识是不正确的。因此,问题本身的部分或全部可能不正确。为了保持原始问题的完整性,为了说明我为什么错了,我决定只添加此通知,并按原样保留原始问题。]

在 C++(非 C)中,全局 const数组使用内部链接进行优化。如果定义为全局const数组放在一个单独的 中.cpp 文件,它会生成一个 undefined reference链接器错误。见 undefined reference to array of constants .

因此,对于每个 .cpp 文件访问相同的常量数组,我们应该使用单独的 const数组,最好是头文件形式,如下例:

foo.h :

const int Arr[10]={1,6,3,5,5,6,8,8,9,20};

foo.cpp :
#include "foo.h"
// ...
memcmp(Arr, MyArr, 10*sizeof(int));

bar.cpp :
#include "foo.h"
// ...
memcmp(Arr, MyArr2, 10*sizeof(int));

问题是:
foo.cppbar.cpp有自己的Arr[] .它们会被合并(优化)成一个拷贝吗?

最佳答案

In C++ (not C), a global const array uses internal linkage for optimization



“优化”可能不是正确的词。默认内部链接
const文件范围对象允许我们定义 const对象
头文件无需前缀 static ,或将它们封装在匿名
命名空间,以防止多定义链接错误。这个方便
和直观。优化可能会产生,也可能不会产生,这取决于这个和那个。

在这方面,“文件范围”当然是比“全局”更好的词。你会
一会儿看看为什么。

在这个分数上,数组没有什么特别之处。全部 const文件范围
默认情况下,对象具有内部链接,在 C++ 中。

因此,也许您的问题可以尖锐化为:C++ 是否保证不同的文件范围 const具有相同名称、类型和字节的不同翻译单元中的对象
值将合并到它们链接的程序中的单个拷贝中?

不,它没有。相反,C++ 标准在一个
具有相同地址的程序(对象和子对象除外):

C++11 [intro.object],第 6 段

Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies. Two objects that are not bit-fields may have the same address if one is a subobject of the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they shall have distinct addresses4.



(强调我的)。后来的标准也有同样大意的词。

该脚注 [4] 提供了一个回旋余地的缝隙:

4) Under the “as-if” rule an implementation is allowed to store two objects at the same machine address or not store an object at all if the program cannot observe the difference.



但是如果不同的对象在程序中是可区分的,那么它们一定不能
有相同的地址 - 如果他们合并,他们会这样做。

并且即使标准没有做这个规定,相同的合并
文件范围const无论如何,来自不同翻译单元的对象都是不可行的。
考虑:

数组.h
#ifndef ARRAY_H
#define ARRAY_H

const int Arr[10]={1,6,3,5,5,6,8,8,9,20};

#endif

foo.cpp
#include "array.h"
#include <iostream>

void foo()
{
    std::cout << "Address of `Arr` in `foo.cpp` = " << Arr << std::endl;
}

bar.cpp
#include "array.h"
#include <iostream>

void bar()
{
    std::cout << "Address of `Arr` in `bar.cpp` = " << Arr << std::endl;

}

main.cpp
extern void foo();
extern void bar();

int main()
{
    foo();
    bar();
    return 0;
}

将所有这些源文件编译为目标文件:
g++ -Wall -c foo.cpp bar.cpp main.cpp

编译器遇到了
const int Arr[10]={1,6,3,5,5,6,8,8,9,20};

编译中foo.cppfoo.o并相应地定义了一个对象
foo.o :
$ readelf -s foo.o | grep Arr
     6: 0000000000000000    40 OBJECT  LOCAL  DEFAULT    5 _ZL3Arr
_ZL3Arr是文件范围符号 Arr 的名称修改:
$ c++filt _ZL3Arr
Arr
40是对象的大小(以字节为单位),适合 10 个 4 字节整数。

对象是LOCAL :
  • LOCAL = 内部链接 = 链接器不可见
  • GLOBAL = 外部链接 = 链接器可见

  • (这就是为什么“文件范围”比“全局”更好的原因)。

    该对象在链接部分中定义,索引为 5foo.o . readelf也可以告诉我们什么联动
    部分是:
    $ readelf -t foo.o
    There are 15 section headers, starting at offset 0x7e0:
    
    Section Headers:
      [Nr] Name
           Type              Address          Offset            Link
           Size              EntSize          Info              Align
           Flags
      [ 0]
           NULL                   NULL             0000000000000000  0000000000000000  0
           0000000000000000 0000000000000000  0                 0
           [0000000000000000]:
      ...
      ...
      [ 5] .rodata
           PROGBITS               PROGBITS         0000000000000000  00000000000000e0  0
           0000000000000053 0000000000000000  0                 32
           [0000000000000002]: ALLOC
      ...
      ...
    

    第 5 节是 .rodata ,这是只读数据。 Arr已放入只读数据
    因为它是 const .

    出于同样的原因,同样的事情也适用于 bar.o :
    $ readelf -s bar.o | grep Arr
         6: 0000000000000000    40 OBJECT  LOCAL  DEFAULT    5 _ZL3Arr
    

    所以每个foo.obar.o包含它自己的 40 字节对象 _ZL3ArrLOCAL和只读。编译全部完成并且
    我们还没有一个程序。所以如果_ZL3Arrfoo.o_ZL3Arrbar.o将要合并到程序中,它们必须由链接器合并。
    即使我们想要它,或者 C++ 允许它,链接器也不能这样做,因为
    链接器看不到它们!

    让我们进行链接并请求链接器的映射文件:
    $ g++ -o prog main.o foo.o bar.o -Wl,-Map=prog.map
    

    Mapfile 命中真正的全局 ( = GLOBAL ) 符号:
    $ grep -Po 'foo' prog.map | wc -w
    12
    $ grep -Po 'bar' prog.map | wc -w
    10
    $ grep -Po 'main' prog.map | wc -w
    8
    

    映射文件命中 Arr :
    $ grep -Po 'Arr' prog.map | wc -w
    0
    

    但是readelf可以看到局部符号,现在我们有了一个程序:
    $ readelf -s prog | grep Arr
        36: 0000000000000b20    40 OBJECT  LOCAL  DEFAULT   16 _ZL3Arr
        42: 0000000000000b80    40 OBJECT  LOCAL  DEFAULT   16 _ZL3Arr
    

    所以prog包含两个 40 字节 LOCAL名称为 _ZL3Arr 的对象,
    两者都在程序的链接部分 16 中,即...
    $ readelf -t prog
    There are 29 section headers, starting at offset 0x2ce8:
    
    Section Headers:
      [Nr] Name
           Type              Address          Offset            Link
    
           Size              EntSize          Info              Align
           Flags
      ...
      ...
      [16] .rodata
           PROGBITS               PROGBITS         0000000000000b00  0000000000000b00  0
           00000000000000d1 0000000000000000  0                 32
           [0000000000000002]: ALLOC
      ...
      ...
    

    再次,只读数据。
    readelf还说第一个_ZL3Arr s 位于程序偏移量 0xb20 ;第二
    0xb80 1. 所以当我们最终运行程序时,我们应该很高兴,
    但并不惊讶,看到:
    $ ./prog
    Address of `Arr` in `foo.cpp` = 0x55edf0dd6b20
    Address of `Arr` in `bar.cpp` = 0x55edf0dd6b80
    

    本地Arr引用 foo()以及 bar() 引用的那个保持
    相距 0x60 个字节,分别在内存中距程序开始处 0xb20 和 0xb80 个字节。

    显然你更喜欢只有一个 Arr ,不是两个,在程序中。到
    实现你必须编译:
    const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
    

    在一个目标文件中,带有外部链接,所以链接器可以在那里看到它,
    并在所有其他对象文件中引用该对象。像这样:

    array.h(修订版)
    #ifndef ARRAY_H
    #define ARRAY_H
    
    extern const int Arr[10];
    
    #endif
    

    array.cpp
    #include "array.h"
    
    const int Arr[10]={1,6,3,5,5,6,8,8,9,20};
    

    其他文件同前。在 array.h我们明确声明 Arr具有外部链接,并且该声明在 array.cpp 中被编译器看到并得到尊重。 .

    编译并链接:
    $ g++ -Wall -c main.cpp foo.cpp bar.cpp array.cpp
    $ g++ -o prog main.o foo.o bar.o array.o
    

    什么是Arr现在算在节目里吗?
    $ readelf -s prog | grep 'Arr'
        60: 0000000000000b80    40 OBJECT  GLOBAL DEFAULT   16 Arr
    

    一。还在只读数据中。但现在GLOBAL .和 prog同意
    只有一个 Arr :
    $ ./prog
    Address of `Arr` in `foo.cpp` = 0x562a4fb7bb80
    Address of `Arr` in `bar.cpp` = 0x562a4fb7bb80
    

    [1] 一些细心的读者可能想知道为什么我们看到的是偏移量而不是绝对地址
    这里。这是因为我的 Ubuntu 17.10 工具链默认生成 PIE 可执行文件。

    关于C++ 全局常量数组 : Is it guaranteed to be merged (optimized) into one copy?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48636462/

    相关文章:

    c++ - 使用用户定义的文字初始化 constexpr 数组

    c++ - 如何存储可变类型的参数?

    c++ - 引用一个可能被破坏的静态对象

    c++ - 如何构造一个由 double 和 ""组成的字符串,然后将其作为 const char* 提供给 cMessage 构造函数?

    c++ - basic_string 的前导/尾随空格不敏感特征

    c++ - 从 C++ 调用 cURL 命令返回意外错误代码,如 1792 和 6656

    c++ - 为什么我的 swap<string,string> 比 std 版本慢得多?

    c++ - [Args...] 为空的可变参数模板的部分模板特化

    c++ - 制作一个 VC++ .exe 到 DLL 。这可能吗?

    c++ - boost::static_visitor 作为映射值