3

[Reader's attention: As a suggestion by a downgrade comment to this question, I add this attention notice: Do not assume any part of this question as a truth statement: I raised the question, partly because some of my knowledge was incorrect. Thus some or all of the question itself, may not be correct. To maintain integral of the original question, to show why I was wrong to it, I decide to just add this notice, and retain the original question as is.]

In C++ (not C), a global const array uses internal linkage for optimization. If the definition for a global const array goes in a separate .cpp file, it will generate an undefined reference linker error. See undefined reference to array of constants.

Thus, for each .cpp file to access a same const array, we should use a separate const array, preferably in header file form, as the example below:

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));

The question is:
Since foo.cpp and bar.cpp has its own Arr[]. Will they be merged (optimized) into one copy?

Robin Hsu
  • 4,164
  • 3
  • 20
  • 37
  • Sorry, how can you use `Arr` as the destination for `memcpy` when it's `const`? – Brian Bi Feb 06 '18 at 06:07
  • typo, sorry, I mean memcmp – Robin Hsu Feb 06 '18 at 06:08
  • You need `extern` on both the declaration and the definition for it to work properly. – Brian Bi Feb 06 '18 at 06:10
  • No. If all uses extern, then, it's an undefined reference error. – Robin Hsu Feb 06 '18 at 06:11
  • 4
    @RobinHsu No, that's not true. If the header file has `extern const int Arr[10];`, and the source file has `extern const int Arr[10] = { ... };`, you won't get an undefined reference. But it means that when compiling other source files, the values held in `Arr` are unknown and the optimiser won't be able to make use of them. –  Feb 06 '18 at 06:13
  • Yes. You are right. Could you rewrite it as an answer? I will just accept it. – Robin Hsu Feb 06 '18 at 06:18
  • @RobinHsu That's the answer to the question you linked to (and posted there already), not an answer to this question. –  Feb 06 '18 at 06:32
  • 2
    All objects (except subobjects of the same object) have different addresses. If your program does not depend on the addresses being different, then the compiler can in principle merge the objects, but normally it is too hard for the compiler to prove. So you should not expect them to be merged. – n. m. could be an AI Feb 06 '18 at 07:15
  • Surely, the linkage rules are that the linker can assume that two speculatively compiled non-static objects with the same (decorated) name are the same thing, and can be merged without real regard to contents. This is effectively what happens with inlinable and template methods when they are not inlined. – Gem Taylor Feb 06 '18 at 14:00
  • @GemTaylor `const` gives the object internal linkage. That means it effectively becomes `static`. –  Feb 06 '18 at 17:02
  • There is a way to get maximum optimization (compiler can see and inline values, and only one copy in the final binary). But you have to give up portability. The thing to do is to use a forward declaration to change the linkage to `extern`, put the array definition into every compilation unit (probably in a header file), and avoid the resulting multiple definition error at link time with a weak symbol annotation. Unfortunately, that annotation is non-standard. In Visual C++, `__declspec(selectany)`, in gcc, `__attribute__((weak))` – Ben Voigt Feb 08 '18 at 20:21
  • Your initial premise is wrong -- you can put `extern const int Arr[10];` in the header and put `const int Arr[10] = {whatever};` in the `.cpp` file, it is perfectly fine. You misunderstood the problem on the question you linked to – M.M Feb 08 '18 at 20:29
  • Sorry but I have to downvote because newbies browsing the site may read the first 2 paragraphs and think that is correct information. Would recommend just removing those 2 paragraphs and leaving the question to be about whether the arrays will be merged. – M.M Feb 08 '18 at 20:43

1 Answers1

2

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

"Optimization" is perhaps not the right word. Default internal linkage for const file-scope objects allows us to define const objects in header files without having to prefix static, or enclose them in an anonymous namespace, to head off multiple-definition linkage errors. This is convenient and intuitive. Optimizations may accrue, or not, depending on this and that.

"File-scope" is certainly a better word that "global" in this connection. You'll see why in a while.

And there's nothing special about arrays on this score. All const file-scope objects have internal linkage by default, in C++.

So maybe your question can be sharpened up as: Does C++ guarantee that distinct file-scope const objects in different translation units that have the same name, type and byte-wise value will be merged to a single copy in a program that they are linked in?

No it doesn't. On the contrary, the C++ Standard probibits distinct objects in a program (other than object and sub-object) from having the same address:

C++11 [intro.object], para 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.

(emphasis mine). Later standards have words to the same effect.

There is a crevice of wriggle-room provided by that footnote [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.

But if distinct objects are distinguishable in a program, then they must not have the same address - which they would do, were they merged.

And even if the Standard did not make this stipulation, the merging of identical file-scope const objects from different translation units would be unfeasible anyway. Consider:

array.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;
}

Compile all those source files to object files:

g++ -Wall -c foo.cpp bar.cpp main.cpp

The compiler encountered a

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

in compiling foo.cpp to foo.o and accordingly defined an object in foo.o:

$ readelf -s foo.o | grep Arr
     6: 0000000000000000    40 OBJECT  LOCAL  DEFAULT    5 _ZL3Arr

_ZL3Arr is the name-mangling of file-scope symbol Arr:

$ c++filt _ZL3Arr
Arr

40 is the size of the object in bytes, which is right for 10 4-byte integers.

The object is LOCAL:

  • LOCAL = internal linkage = invisible to the linker
  • GLOBAL = external linkage = visible to the linker

(That's why "file-scope" was a better word than "global").

The object is defined in the linkage section with index 5 in foo.o. readelf can also tell us what linkage section that is:

$ 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
  ...
  ...

Section 5 is .rodata, that is readonly data. Arr has been put in read-only data because it's const.

For the same reasons, the same things are all true of bar.o:

$ readelf -s bar.o | grep Arr
     6: 0000000000000000    40 OBJECT  LOCAL  DEFAULT    5 _ZL3Arr

So each of foo.o and bar.o contains its own 40-byte object _ZL3Arr that is LOCAL and read-only. Compilation is all done and we haven't got a program yet. So if the _ZL3Arr in foo.o and the _ZL3Arr in bar.o were going to be merged in the program, they'd have to be merged by the linker. And even if we wanted it to, or C++ allowed it to, the linker can't do that, because the linker can't see them!

Let's do the linkage and ask for the linker's mapfile:

$ g++ -o prog main.o foo.o bar.o -Wl,-Map=prog.map

Mapfile hits for the really global ( = GLOBAL) symbols:

$ grep -Po 'foo' prog.map | wc -w
12
$ grep -Po 'bar' prog.map | wc -w
10
$ grep -Po 'main' prog.map | wc -w
8

Mapfile hits for Arr:

$ grep -Po 'Arr' prog.map | wc -w
0

But readelf can see local symbols, and now we've got a program:

$ readelf -s prog | grep Arr
    36: 0000000000000b20    40 OBJECT  LOCAL  DEFAULT   16 _ZL3Arr
    42: 0000000000000b80    40 OBJECT  LOCAL  DEFAULT   16 _ZL3Arr

So prog contains two 40 byte LOCAL objects by the name of _ZL3Arr, both in linkage section 16 of the program, which is...

$ 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
  ...
  ...

once again, the read-only data.

readelf also said that the first of those _ZL3Arrs is at program offset 0xb20; the second is at 0xb801. So when we finally run the program we should be pleased, but not surprised, to see that:

$ ./prog
Address of `Arr` in `foo.cpp` = 0x55edf0dd6b20
Address of `Arr` in `bar.cpp` = 0x55edf0dd6b80

the local Arr referenced by foo() and the one referenced by bar() remain 0x60 bytes apart, respectively 0xb20 and 0xb80 bytes from the start of the program in memory.

Evidently you would prefer to have just one Arr, not two, in the program. To achieve that you have to compile:

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

in just one object file, with external linkage, so the linker can see it there, and refer to that one object in all other object files. Like so:

array.h (revised)

#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};

Other files as before. In array.h we are expressly declaring that Arr has external linkage, and that declaration in seen and honoured by the compiler in array.cpp.

Compile and link:

$ g++ -Wall -c main.cpp foo.cpp bar.cpp array.cpp
$ g++ -o prog main.o foo.o bar.o array.o

What's the Arr count in the program now?

$ readelf -s prog | grep 'Arr'
    60: 0000000000000b80    40 OBJECT  GLOBAL DEFAULT   16 Arr

One. Still in the read-only data. But now GLOBAL. And prog agrees that there is only one Arr:

$ ./prog
Address of `Arr` in `foo.cpp` = 0x562a4fb7bb80
Address of `Arr` in `bar.cpp` = 0x562a4fb7bb80


[1] Some close readers might wonder why we see offsets rather than absolute addresses here. It's because my Ubuntu 17.10 toolchain make PIE executables by default.
Mike Kinghan
  • 55,740
  • 12
  • 153
  • 182