const char 编译器优化

const char compiler optimization

我在两个不同的文件中有全局 const char 定义:

f1:

const char foo1[] = "SAME_VALUE";

f2:

const char foo2[] = "SAME_VALUE";

想了解在最终的二进制文件中是否会对其进行优化以在内存中使用 common space。这是在 GCC

的上下文中

阅读 C 标准,如 n1570. It requires that foo1 != foo2 when that test happens at runtime (after of course extern const char foo1[]; extern const char foo2[]; declarations). It could accept that a compiler optimizes if (foo1==foo2) abort(); (or assert(foo1 != foo2); after some #include <assert.h>, see assert(3)...) 到无操作。

Wanted to understand if in the final binary this will be optimized to take common space in memory.

这是非常特定于编译器的

也许 GCC invoked as gcc -flto -O3(可能 -fwhole-program)在编译和 link 时可以优化它。

如果内存 space 在您的项目中非常重要,请考虑编写 GCC plugin -or some GNU Binutils extension- to detect (and perhaps optimize) that case. Such a plugin could use some sqlite 数据库(在编译时)管理所有全局 const char 定义。

请注意,您想要的优化要求编译器确实检测到像 foo1 == foo2 这样的指针相等 never 测试(并且 const char*p1, *p2; 它永远不会碰巧 p1foo1p2foo2 并且指针相等性 p1 == p2 在程序运行时进行测试)。您可以使用 Frama-C 之类的工具来确保这一点。

更多的编译器正在转换

const char foo1[] = "SAME VALUE";
const char foo2[] = "VALUE";

相当于

const char foo1[] = "SAME VALUE";
const char foo2[] = foo1 + 5; //// since strlen("SAME ") is 5

我的建议是在你的构建过程中生成 C 代码,很好地记录它,并明确地共享这些数据。

另一种方法可能是使用预处理器(可能在 GNU m4 or GPP) or write your GCC plugin 之上定义一些 __builtin_my_shared_string 编译器内置。

您稍后评论:

Code is being generated by a tool/script and there are many such constants and files.

然后只需改进 tool/script 即可生成更好的代码。您的 C 生成工具可以使用一些 sqlite 数据库。

PS。 AFAIK,GCC 和 Clang 都可以进行这样的优化,但我不确定。由于它们是开源的,您可以改进它们。

PPS。您的问题可能是 Bismon static source code analyzer and related to both CHARIOT and DECODER 项目的用例。这似乎比你想象的要难。你可以联系这些项目的负责人。

这种优化称为string interning

GCC 默认设置 -fmerge-constants 标志:

Attempt to merge identical constants (string constants and floating-point constants) across compilation units.
This option is the default for optimized compilation if the assembler and linker support it. Use -fno-merge-constants to inhibit this behavior.
Enabled at levels -O, -O2, -O3, -Os.

让我们用名为 f.c 的第三个文件创建一个可执行文件来引用字符串:

#include <stdio.h>

// For proposition#1
extern const char foo1[], foo2[];

// For proposition#2
//extern const char *foo1, *foo2;

int main(void) {

  printf("%s\n", foo1);
  printf("%s\n", foo2);

  return 0;
}

当你在f1.cf2.c中分别定义如下(命题#1):

const char foo1[] = "SAME_VALUE";
const char foo2[] = "SAME_VALUE";

这导致 2 个不同的内存空间,其中存储了字符串“SAME_VALUE”。所以,字符串是重复的:

$ gcc f.c f1.c f2.c
$ objdump -D a.out
[...]
0000000000001060 <main>:
    1060:   f3 0f 1e fa             endbr64 
    1064:   48 83 ec 08             sub    [=13=]x8,%rsp
    1068:   48 8d 3d 99 0f 00 00    lea    0xf99(%rip),%rdi <-- foo1@2008
    106f:   e8 dc ff ff ff          callq  1050 <puts@plt>
    1074:   48 8d 3d 9d 0f 00 00    lea    0xf9d(%rip),%rdi <-- foo2@2018
    107b:   e8 d0 ff ff ff          callq  1050 <puts@plt>
[...]
0000000000002008 <foo1>:
    2008:   53        'S'  <-- 1 string @ 2008
    2009:   41        'A'
    200a:   4d        'M' 
    200b:   45 5f     'E' '_'
    200d:   56        'V'
    200e:   41        'A'
    200f:   4c 55     'L' 'U'
    2011:   45        'E'
    ...

0000000000002018 <foo2>:
    2018:   53        'S'  <-- Another string @ 2018
    2019:   41        'A' 
    201a:   4d        'M' 
    201b:   45 5f     'E' '_'
    201d:   56        'V'
    201e:   41        'A'
    201f:   4c 55     'L' 'U' 
    2021:   45        'E'

但是如果在f1.cf2.c中分别定义如下(命题#2):

const char *foo1 = "SAME_VALUE";
const char *foo2 = "SAME_VALUE";

你定义了两个指向同一个字符串的指针。在这种情况下,“SAME_VALUE”可能不会被复制。在下面的原始反汇编中,字符串位于地址 2004,foo1foo2 都指向它:

$ gcc f.c f1.c f2.c
$ objdump -D a.out
[...]
    2004:   53        'S'    <-- 1 string @ 2004
    2005:   41        'A'
    2006:   4d        'M'
    2007:   45 5f     'E' '_'
    2009:   56        'V'
    200a:   41        'A'
    200b:   4c 55     'L' 'U'
    200d:   45        'E'
[...]
0000000000001060 <main>:
    1060:   f3 0f 1e fa             endbr64 
    1064:   48 83 ec 08             sub    [=16=]x8,%rsp
    1068:   48 8b 3d a1 2f 00 00    mov    0x2fa1(%rip),%rdi <-- 106f+2fa1=foo1@4010 
    106f:   e8 dc ff ff ff          callq  1050 <puts@plt>
    1074:   48 8b 3d 9d 2f 00 00    mov    0x2f9d(%rip),%rdi <-- 107b+2f9d=foo2@4018 
[...]
0000000000004010 <foo1>:
    4010:   04 20         <-- foo1 = @2004
[...]
0000000000004018 <foo2>:
    4018:   04 20         <-- foo2 = @2004

为了避免与 proposition#1 重复,GCC 提供 -fmerge-all-constants:

Attempt to merge identical constants and identical variables.
This option implies -fmerge-constants. In addition to -fmerge-constants this considers e.g. even constant initialized arrays or initialized constant variables with integral or floating-point types. Languages like C or C++ require each variable, including multiple instances of the same variable in recursive calls, to have distinct locations, so using this option results in non-conforming behavior.

让我们用这个标志重建proposition#1。我们可以看到foo2被优化掉了,只保留和引用了foo1

$ gcc -fmerge-all-constants f.c f1.c f2.c
$ objdump -D a.out
[...]
0000000000001149 <main>:
    1149:   f3 0f 1e fa             endbr64 
    114d:   55                      push   %rbp
    114e:   48 89 e5                mov    %rsp,%rbp
    1151:   48 8d 3d b0 0e 00 00    lea    0xeb0(%rip),%rdi <-- 1158(RIP) + eb0 = 2008 <foo1>
    1158:   e8 f3 fe ff ff          callq  1050 <puts@plt>
    115d:   48 8d 3d a4 0e 00 00    lea    0xea4(%rip),%rdi <-- 1164(RIP) + ea4 = 2008 <foo1>
    1164:   e8 e7 fe ff ff          callq  1050 <puts@plt>
    1169:   b8 00 00 00 00          mov    [=17=]x0,%eax
[...]
0000000000002008 <foo1>:
    2008:   53    'S' <--- foo2 optimized out, only foo1 defined
    2009:   41    'A'
    200a:   4d    'M'
    200b:   45 5f 'E' '_'
    200d:   56    'V'
    200e:   41    'A'
    200f:   4c 55 'L' 'U'
    2011:   45    'E'