2016-11-16 62 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是完全有可能的。 –