2016-11-16 52 views
1

我有一个我在Android中使用的库,但我确定这个问题并非特定于Android。 这个库包含一系列打印到logcat的错误代码,并且它们全部由一个常量字符串组成。库中的`const char *`存储的奇怪行为.s​​o文件

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

我虽然,与印刷的一个小的额外费用(这应该是很好的,因为这些代码很少使用),我可以节省大量的空间,如果我在第2个部分拆分字符串。然后编译器应该完成这个工作,并且将一个字符串中的公共部分分组。由于编译器具有重复的字符串移除优化步骤(CLANG和GCC)。

我就是这么做的:(我有很多这样的,但他们都有这样的模式,我知道我应该使用一个定义(但这是一个快速测试))

... 
if(...){ALOGE("Error in parameter %s","XXXXXX");} 
if(...){ALOGE("Error in parameter %s","YYYYYY");} 
if(...){ALOGE("Error in parameter %s","ZZZZZZ");} 
... 

什么我发现是这样的:

  1. 该库是完全相同的大小。 .rodata现在小得多,但.text增加了几乎相同的数额。 (只有几个字节的差别)
  2. strings命令现在打印1次只有"Error in parameter %s"字符串,和分离的部分。所以没有字符串合并发生。
  3. 似乎没有,如果我在32位,64位等编译到重要..

那么,究竟是怎么回事?我该如何解决?任何指导?编译器在做什么? 由于

额外数据:

  • 编译CLANG 4.9(4.8确实相同的结果)。
  • 标志:-Os -fexceptions -std = C++ 11 -fvisivility =隐藏

编辑:

我使用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; 
} 

NonSp点亮:

#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!               
+1

'.text'用于代码 – Danh

+1

您使用什么编译器?这是C代码还是C++代码?你使用了哪些优化设置? –

+0

这是C++,标志没有什么特别的。只是-Os。如果我没有记错,编译器是叮当声4.9(ndk r13b) – DarkZeros

回答

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。

+0

哦,谢谢你,我明白了!实际上,在这种情况下(4096),有一个4k大小的臃肿,所以也许这就是原因。如果你没有设法将整个页面的代码大小缩小,编译器仍然会在'.rodata'中为0分配页面,即使页面只包含一个字节。所以我应该要么进一步减少我的弦乐,要么根本不做任何事情。明天我会检查正确的代码来查看.rodata的页面数量。 – DarkZeros

+0

的确我在'.rodata'中使用了0x66f8。通过我的优化,0x60f0。但由于页面大小为0x1000,所以我没有看到大小的下降。但很高兴知道这些优化是可能的! – DarkZeros

+0

@DarkZeros尽管如此 - 它的工作原理可能与未来的其他技巧或优化一起可能会有所帮助。 – Sergio

1

你只节省每串19个字节,但代价传递一个额外的论据到什么看起来像可变参数函数。至少是一个加载地址和一个推送。

让我猜,ALOGE实际上是一个宏吗?

我不认为你需要一个DEFINE - 你需要一个函数(内联),如:

void BadParameter(const char * paramName) 
{ 
    ALOGE("Error in parameter %s", paramName); 
} 

...并更换所有的调用。

+0

我明白了,但我真正的代码有更长的字符串,我做了数字,我应该保存约1kB的字符串,奇怪的是,大小是完全相同的。我做了一个具有相同结果的在线示例。 – DarkZeros

+1

请记住,可执行文件大小很可能是整数个页面。我不知道在这方面有多大的“页面”,但4k是完全有可能的。 –