库中的`const char *`存储的奇怪行为.s​​o文件

时间:2016-11-16 11:12:52

标签: android c++ c string

我有一个我在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");}
...

我发现的是:

  1. 图书馆的大小完全相同。 .rodata现在要小得多,但.text增加了几乎相同的数量。 (仅限几个字节)
  2. strings命令现在仅打印1次"Error in parameter %s"字符串和分隔的部分。所以没有发生字符串合并。
  3. 如果我在32位,64位等编译,似乎并不重要。
  4. 那么,这里发生了什么?我该怎么办?任何指导?编译器在做什么? 感谢

    额外数据:

    • 编译器CLANG 4.9(4.8做同样的结果)。
    • 标志:-Os -fexceptions -std = c ++ 11 -fvisivility = hidden

    编辑:

    我使用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!                                                          
    

2 个答案:

答案 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);
}

...并用它替换所有的电话。