我有一个我在Android中使用的库,但我很确定这个问题并非特定于Android。 这个库包含一堆我打印到logcat的错误代码,所有错误代码都由一个常量字符串组成。
...
if(...){ALOGE("Error in parameter XXXXXX");}
if(...){ALOGE("Error in parameter YYYYYY");}
if(...){ALOGE("Error in parameter ZZZZZZ");}
...
今天我注意到我的.rodata部分有大量数据(大约16kB)。所以我运行strings mylib.so
,我得到了一堆字符串。
Error in parameter XXXXXX
Error in parameter YYYYYY
Error in parameter ZZZZZZ
但是,由于打印成本较低(这应该很好,因为很少使用这些代码),如果我将字符串分成两部分,我可以在空间中节省很多。然后编译器应该完成这项工作,并在一个字符串中组成公共部分。由于编译器具有重复的字符串删除优化步骤(CLANG和GCC)。
我是这样做的:(我有很多这样的,但他们都有这样的模式,我知道我应该使用一个定义(但这是一个快速测试))
...
if(...){ALOGE("Error in parameter %s","XXXXXX");}
if(...){ALOGE("Error in parameter %s","YYYYYY");}
if(...){ALOGE("Error in parameter %s","ZZZZZZ");}
...
我发现的是:
.rodata
现在要小得多,但.text
增加了几乎相同的数量。 (仅限几个字节)strings
命令现在仅打印1次"Error in parameter %s"
字符串和分隔的部分。所以没有发生字符串合并。那么,这里发生了什么?我该怎么办?任何指导?编译器在做什么? 感谢
额外数据:
编辑:
我使用GCC创建了一个在线示例测试相同的结果Online GCC
分割:
#include <stdio.h>
int main()
{
int a = rand()%7;
switch(a){
case 0: printf("Hello, %s!\n","Anna"); break;
case 1: printf("Hello, %s!\n","Bob"); break;
case 2: printf("Hello, %s!\n","Clark"); break;
case 3: printf("Hello, %s!\n","Danniel"); break;
case 4: printf("Hello, %s!\n","Edison"); break;
case 5: printf("Hello, %s!\n","Foo"); break;
case 6: printf("Hello, %s!\n","Garret"); break;
}
return 0;
}
NonSplit:
#include <stdio.h>
int main()
{
int a = rand()%7;
switch(a){
case 0: printf("Hello, Anna!\n"); break;
case 1: printf("Hello, Bob!\n"); break;
case 2: printf("Hello, Clark!\n"); break;
case 3: printf("Hello, Danniel!\n"); break;
case 4: printf("Hello, Edison!\n"); break;
case 5: printf("Hello, Foo!\n"); break;
case 6: printf("Hello, Garret!\n"); break;
}
return 0;
}
编译:
gcc -Os -o main main.c
gcc -Os -o main2 main2.c
尺寸:
-rwxr-xr-x 1 20446 20446 8560 Nov 16 11:43 main
-rw-r--r-- 1 20446 20446 478 Nov 16 11:41 main.c
-rwxr-xr-x 1 20446 20446 8560 Nov 16 11:42 main2
-rw-r--r-- 1 20446 20446 443 Nov 16 11:39 main2.c
的字符串:
strings main2 | grep "Hello"
Hello, Anna!
Hello, Bob!
Hello, Clark!
Hello, Danniel!
Hello, Edison!
Hello, Foo!
Hello, Garret!
strings main | grep "Hello"
Hello, %s!
答案 0 :(得分:2)
您的所有期望都相当正确,但测试用例不足以证明其效果。首先,二进制可执行文件具有“段/段对齐”(或类似的东西)的概念。简而言之,这意味着不同部分的第一个字节只能放在文件偏移量上,这些偏移量是某个值的倍数(例如十进制512
)。部分之间未使用的空间用零填充以满足此要求。并且您的测试用例提供的所有数据都不会耗尽该填充,因此您无法感受到真正的差异。接下来 - 如果你想更清楚地比较效果 - 你不应该链接启动代码,即你应该使用最少数量的引用而不是常规可执行文件来构建动态库。
接下来,我的测试程序。它与你的有点不同。但概念上并非如此。
#include <stdio.h>
#if defined(_SPLIT)
#define LOG(str) printf("Very very very loooo-o-o-o-o-o-o-ooooong prefix %s", str )
#elif defined(_NO_SPLIT)
#define LOG(str) printf("Very very very loooo-o-o-o-o-o-o-ooooong prefix " str )
#else
#error "Don't know what you want."
#endif
int foo(void) {
LOG("aaaaaaaa");
LOG("bbbbbbbb");
LOG("cccccccc");
LOG("dddddddd");
LOG("eeeeeeee");
LOG("ffffffff");
LOG("gggggggg");
LOG("hhhhhhhh");
LOG("iiiiiiii");
LOG("jjjjjjjj");
LOG("kkkkkkkk");
LOG("llllllll");
LOG("mmmmmmmm");
LOG("nnnnnnnn");
LOG("oooooooo");
LOG("pppppppp");
LOG("qqqqqqqq");
LOG("rrrrrrrr");
LOG("ssssssss");
LOG("tttttttt");
LOG("uuuuuuuu");
LOG("vvvvvvvv");
LOG("wwwwwwww");
LOG("xxxxxxxx");
LOG("yyyyyyyy");
LOG("zzzzzzzz");
return 0;
}
然后,让我们创建动态库:
$ gcc --shared -fPIC -o t_no_split.so -D_NO_SPLIT test.c
$ gcc --shared -fPIC -o t_split.so -D_SPLIT test.c
并比较尺寸:
-rwxr-xr-x 1 sysuser sysuser 12098 Nov 16 14:19 t_no_split.so
-rwxr-xr-x 1 sysuser sysuser 8002 Nov 16 14:19 t_split.so
IMO,确实存在显着差异。而且,说实话,我没有检查每个部分的尺寸,但无论如何你可以自己做。
当然,这并不意味着不会分割字符串使用12098 - 8002
字节而不是分割字符串。这只意味着编译器/链接器必须为t_no_split.so
使用比t_split.so
更多的空间。而这种膨胀肯定是由字符串大小的差异引起的。另一个有趣的事情 - 拆分甚至可以抵消因将第二个参数传递给printf()
而导致的机器代码的小膨胀。
P.S。我的机器是x64 Linux,GCC 4.8.4。
答案 1 :(得分:1)
每个字符串只保存19个字节,但代价是将其他参数传递给varargs函数。至少是一个加载地址和推送。
我猜,ALOGE
实际上是一个宏?
我认为你不需要DEFINE - 你需要一个函数( not inline),如:
void BadParameter(const char * paramName)
{
ALOGE("Error in parameter %s", paramName);
}
...并用它替换所有的电话。