常量字符编译器优化

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

f1:

const char foo1[] = "SAME_VALUE";

f2:
const char foo2[] = "SAME_VALUE";

想了解在最终的二进制文件中,是否会优化以占用内存中的共同空间。这是在GCC的背景下。

1
这取决于编译器。MSVC将使用/GF开关优化字符串字面量(称为字符串池)。也许其他人可以提供gcc和clang的答案。 - Michaël Roy
这是关于GCC的,已更新描述。 - Karun
值得注意的是,这个问题的微妙之处在于const关键字。这不是char * foochar foo[]的比较,前者是常量,后者是可修改的。问题是关于const char foo[]const关键字是否足以让编译器理解它可以被视为char *foo - Roberto Caboni
字符串常量本身可能只存储一次。这种优化技术称为“字符串池”。我不记得启用/禁用它的gcc开关。 - Lundin
代码是由工具/脚本生成的,有许多这样的常量和文件。如果编译器不能处理它,那么工具需要增强使用一个公共池。 - Karun
显示剩余2条评论
2个回答

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 中分别定义以下内容时(命题#1):
const char foo1[] = "SAME_VALUE";

const char foo2[] = "SAME_VALUE";

这会导致字符串"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中分别定义以下内容(命题#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

为避免与proposition#1重复,GCC提供了-fmerge-all-constants选项:
尝试合并相同的常量和相同的变量。此选项意味着-fmerge-constants。除了-fmerge-constants之外,还考虑了例如具有整数或浮点类型的初始化常量数组或初始化常量变量等内容。像C或C++这样的语言要求每个变量(包括递归调用中相同变量的多个实例)都具有不同的位置,因此使用此选项会导致不符合规范的行为。
让我们使用此标志重新构建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    $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'

即使它们在两个不同的文件中声明? - Karun
是的。我已经更新了我的答案,并附上了代码反汇编。 - Rachid K.
非常感谢,这是一个编译器相关的优化吗? - Karun
1
我讨论了GCC。其他的我不知道。我用有关GCC选项(-fmerge-constants=default和-fmerge-all-constants)的详细信息更新了我的答案。使用第二个选项,即使您的代码提议也会得到优化。 - Rachid K.

2
阅读 C 标准,如 n1570。它要求在运行时(当然是在 extern const char foo1[]; extern const char foo2[]; 声明之后),foo1 != foo2 为真。编译器可以将 if (foo1==foo2) abort();(或一些 #include <assert.h> 后的 assert(foo1 != foo2);,见 assert(3)...)优化为无操作。

想了解最终二进制文件是否会被优化以共享内存空间。

这非常依赖于编译器。

也许在编译和链接时都使用 gcc -flto -O3(以及可能的 -fwhole-program)调用 GCC 可以优化它。
如果内存空间对您的项目非常重要,请考虑编写您的GCC插件-或一些GNU Binutils扩展程序-来检测(并可能优化)该情况。这样的插件可以使用一些 sqlite 数据库(在编译时)来管理所有全局的const char定义。
注意,您想要的优化需要编译器检测到指针相等性,例如foo1 == foo2从未被测试(并且使用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 m4GPP之上)或编写GCC插件定义一些__builtin_my_shared_string编译器内置。
您后来评论说:

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

然后只需改进该工具/脚本以生成更好的代码。您的C生成工具可以使用一些sqlite数据库。
PS. 据我所知,GCC和Clang都可以进行这种优化,但我不确定。而且由于它们是开源的,您可以对它们进行改进。
附言:你的问题可能是Bismon静态源代码分析器的一个用例,与CHARIOT和DECODER项目相关。它似乎比你想象的更困难。你可以联系这些项目的负责人。

/GF将为MSVC完成此操作。在Visual Studio中,发布版本默认启用它。 - Michaël Roy
我听说GCC也可以做到这一点。你需要检查一下。 - Basile Starynkevitch
我非常确定它可以,但我已经被困在使用MSVC编程多年了,可能还会再困一段时间。 :( - Michaël Roy

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