const char 编译器优化

时间:2021-02-12 07:07:19

标签: c compiler-optimization string-interning

我在两个不同的文件中有全局常量字符定义:

f1:

const char foo1[] = "SAME_VALUE";

f2:

const char foo2[] = "SAME_VALUE";

想了解在最终二进制文件中是否会对其进行优化以占用内存中的公共空间。这是在海湾合作委员会的背景下

2 个答案:

答案 0 :(得分:5)

这种优化称为string interning

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

<块引用>

尝试跨编译单元合并相同的常量(字符串常量和浮点常量)。
如果汇编器和链接器支持,则此选项是优化编译的默认选项。使用 -fno-merge-constants 来抑制这种行为。
在 -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 中定义以下内容时(proposition#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    $0x8,%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 (proposition#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    $0x8,%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

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

<块引用>

尝试合并相同的常量和相同的变量。
此选项意味着 -fmerge-constants。除了 -fmerge-constants 之外,这还考虑了例如甚至常量初始化数组或具有整数或浮点类型的初始化常量变量。 C 或 C++ 等语言要求每个变量(包括递归调用中同一变量的多个实例)具有不同的位置,因此使用此选项会导致不一致的行为。

让我们用这个标志重建提议#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    $0x0,%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'

答案 1 :(得分:2)

阅读像 n1570 这样的 C 标准。它要求 foo1 != foo2 在运行时发生该测试时(当然在 extern const char foo1[]; extern const char foo2[]; 声明之后)。它可以接受编译器将 if (foo1==foo2) abort();(或在某个 assert(foo1 != foo2); 之后的 #include <assert.h>,参见 assert(3)...)优化为无操作。

<块引用>

想了解在最终二进制文件中是否会对其进行优化以占用内存中的公共空间。

这是特定于编译器的

也许 GCC 在编译和链接时调用 as gcc -flto -O3(可能还有 -fwhole-program)可以优化它。

如果内存空间在您的项目中非常重要,请考虑编写您的 GCC plugin - 或一些 GNU Binutils 扩展 - 以检测(并可能优化)这种情况。这样的插件可以使用一些 sqlite 数据库(在编译时)来管理所有全局 const char 定义。

请注意,您想要的优化要求编译器确实检测到像 foo1 == foo2 这样的指针相等性从不被测试(并且对于 const char*p1, *p2;,{{1 }} 是 p1foo1p2 并且指针相等性 foo2 在您的程序运行时进行测试)。您可以使用 Frama-C 之类的工具来确保这一点。

更多的编译器正在转换

p1 == p2

相当于

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

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

另一种方法是使用您的预处理器(可能高于 GNU m4GPP)或编写您的 GCC plugin 定义一些内置的 const char foo1[] = "SAME VALUE"; const char foo2[] = foo1 + 5; //// since strlen("SAME ") is 5 编译器。

您稍后发表评论:

<块引用>

代码是由工具/脚本生成的,并且有很多这样的常量和文件。

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

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

PPS。您的问题可能是 Bismon 静态源代码分析器的一个用例,并且与 CHARIOTDECODER 项目相关。这似乎比你想象的要困难。您可以联系这些项目的负责人。

相关问题