【Android Linux內存及性能優化】(三) 進程內存的優化 - ELF執行文件的 數據段-代碼段


本文接着
【Android Linux內存及性能優化】(一) 進程內存的優化 - 堆段
【Android Linux內存及性能優化】(二) 進程內存的優化 - 棧段 - 環境變量 - ELF


一、內存篇

1.1 系統當前可用內存

1.2 進程的內存使用

1.3 進程內存優化

1.3.1 執行文件

1.3.1.5 數據段

1.3.1.5.1 .bss 與 .data 的區別
  • .bss : 主要用來保存未初始化或初始化爲0 的全局變量或靜態變量。
  • .data: 主要用來保存初始化不爲0 的全局變量或靜態變量。

爲什麼初值是否爲0 ,變得如此關鍵?
主要是因方loader 可以對初值爲0 的變量採取一定的優化措施。
loader 在加載進程時,會使用 mmap ,將 ELF 文件的數據段映射到內存中。

  1. 對於那些初值不爲0 的位於數據段的變量,其初始值必須保存在 .data節,需要佔用文件大小,當訪問這些變量時,便會觸發頁故障,將文件中對應的初值回載到內存中,完成初始化。
  2. 對於那些初值爲0 的位於數據段的變量,不必將初始值保存到文件中,loader 只需要將這些段內存映射到一個全0 的頁面即可,這樣.bss 節並不佔據ELF 文件的空間。後續使用時,這些未始初化的變量會在堆段中分配內存,

還有一個差別就是,當程序讀取 data 節的數據時,系統會觸發頁故障,從而分配相應的物理內存
當程序讀取 bss 節的數據時,內核會將其轉到一個全零的頁面,不會觸發頁故障,也不會爲其分配物理內存


另外,在LInux 內核中,內存管理是以頁面爲單位,進程的數據段也必須是頁面對齊的,可問題是數據段做映射時,.data 往往無法正好填滿最後一個頁面,會剩餘一些字節,這時 loader 會試圖用 .bss 節的數據去填充它
也正因如此,在最後的一個頁面中,.data 節填充後剩餘的字節使用 .bss 節數據進行填充,並將這些剩餘的字節全部填充爲0,
這同時會造成對最後一個頁面的寫操作,產生 dirty page。


1.3.1.5.2 C 代碼中變量所在的區域
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#define b 10			// 宏定義,在預處理時替換,10被認爲是立即數,編譯到代碼中
#define c "123"			// 123 被認爲是字符常量,編譯到 .rodata 節中

int bss[10]={0};		// 初始化爲全0 的全局變量,保存在.bss 節, 位於數據段
int data[10]={1};		// 初始化不爲0 的全局變量,保存在.data 節, 位於數據段

const int a = 10;		// 初始化不爲0的全局變量,前面有個const,說明只讀,保存在.rodata 節,位地代碼段
static int m1;			// 沒有初始化的全局靜態變量,位於 .bss 節,位於數據段

int main(){
	static int m2;		// 不是全局變量,但前面有static ,是不初始化的靜態局部變量,位於 .bss 節
	pid_t pid = getpid();	// 局部變量,在進程運行期間,保存在 棧stack 中
	return 0;
}
  1. nm -f sysv xxx 可以查看可執行程序的 變量保存位置

在這裏插入圖片描述
在這裏插入圖片描述


1.3.1.5.3 C++ 代碼中變量所在的區域
  • C++ 內置類型的全局變量

下面再來看下 C++ 類的內存分佈情況:

class MyClass {
public: 
	static int nCount;		// 4 個字節,靜態變量,在MyClass所定義的對象之間共享,位於程序的數據段
	int nValue;				// 4 個字節
	char c;					// 雖然使用了 1個字節,由於內存對齊的緣故,佔用了 4個字節
	MyClass();				// 函數,位於代碼段
	virtual ~MyClass();		// 函數,位於代碼段

	int getValue(void);		// 函數,位於代碼段
	virtual void foo(void);	// 函數,位於代碼段,會增加 4 個字節的空間
	static void addCount();	// 函數,位於代碼段
}

因此,使用 sizeof() 得到的 MyClass 對象的大小爲 12 個字節。

總結:

  1. 靜態成員 和 非靜態成員函數, 主要佔據代碼段內存,生成對象,不會再佔用內存,對象之間共享。
  2. 非靜態數據 是影響對象佔據內存大小的主要因素,隨着對象數據的增加,非靜態數據成員佔據的內存會相應增加。
  3. 所有的對象共享一份靜態數據成員,所以靜態數據成員佔據的內存數量不會隨着對象的數據的增加而增加。
  4. 如果對象中包含虛函數,位於代碼段,會增加 4 個字節的空間,不論有多少個虛函數。

  • C++ 非內置類型的全局變量
    先來看個代碼:
#include <stdio.h>
#include <stdlib.h>

class MyClass{
public:
	MyClass();			//函數,位於代碼段
	MyClass(int i);		//函數,位於代碼段
	~MyClass();			//函數,位於代碼段
	int n1;				// 非靜態數據,位於數據段 .data ,佔用 4 個字節
};

MyClass::MyClass(){
	n1 = 0;				// 局部變量,位於棧stack 中
	printf("n1 = %d\n", n1);
}

MyClass::MyClass(int i){
	n1 = i;				// 局部變量,位於棧stack 中
	printf("n1 = %d\n", n1);
}

MyClass::~MyClass(){
}

MyClass g1;			// 未賦值的全局變量,位於 .bss 節
MyClass g2 = 10;	// 賦了初值的全局對象,實際上也 位於 .bss 節

int main(){
	pause();
	return 0;
}

驗證結果如下:
在這裏插入圖片描述

程序運行後,結果爲:

在這裏插入圖片描述
問題來了,main函數裏面的第一句話是 pause(),
所以剛進入main() 函數應停止了,但依然能看到 g1 和 g2 的構造函數打印出來的結果,
很顯然進入 main() 函數之前,運行了 g1 和 g2 的構造函數。

原因是因爲,loader 在將程序焦點轉移到 main 函數之前,會運行 .init_array 函數指針數組中的所有函數。
查看 .init_array 中都有哪些函數。

在這裏插入圖片描述
108fc 是內存地址的一個序號,不必管它。
7c84000014870000 纔是 init_array 中直正的內容,這裏的內容是以小端排序,翻譯成大端如下:
7c840000 應該爲 0000847c
14870000 應該爲 00008714

通過查看符號表,看看這兩個地址對應着什麼內容 :
在這裏插入圖片描述
在這裏插入圖片描述
這就很清楚了,進程運行時,在調用 main() 之前,要運行 frame_dummyglobal constructors keyed to _ZN7MyclassC2Ev

下面總結下非內置全局變量的初始化過程:

  1. G++ 在編譯時,爲這些非內置類型的全局變量,在 .bss 節預留了內存空間。同時在 .init_array 節中,安排了全局對象的構造函數。
  2. 在程序運行時,在mmap 將數據段映射進入內存之後,調用位於 .init_array 節的全局對象的構造函數,將全局對象創建出來。
  3. 執行 main 函數內的代碼。

注意

  • 對於C 語言編寫的進程來講,在運行時,只是通過 mmap 爲其數據段分配了一段虛擬內存,只有在實際用到纔會分配物理內存
  • 對於 C++ 編寫的程序來講,那些非內置類型的全局變量,由於在main 函數之前,需要運行構造函數,爲其成員變量賦值,這時,雖然在你的程序還沒用到,但它已經開始佔用物理內存了,並且有些非內置類型的全局對象,可能在進程的啓動過程中根本用不到,而其仍然佔用了物理內存,從而造成內存的浪費

1.3.1.5.4 關於數據段的優化

一提到數據段的優化,有人可能想到,一個整型全局變量也就 4Byte,爲這幾個字節值得嗎?

其實:

  1. 減少一個整型變量,不會只減少 4Byte,可能並不會減少內存的使用,也可 能會減少 4KB 的物理內存
    這是因爲在Linux 內核中,內存分配的最小單位是4KB, 是否減少內存的使用主要是看數據段最後一個頁面中所使用的內存大小。
    如果最後一個頁面只使用了4 B,那減少一個整型的全局變量,會節省出 4KB 的內存使用。
    如果最後一個頁面使用了不止4B,那減少一個整型的全局變量,並不會節省內存的使用。

  2. 對於執行文件的數據段的優化,影響有限: 但如果對動態庫的數據段進行優化,其作用將會比較明顯
    比如,一個動態庫被 50 個進程所依賴,那麼在這50 個進程同時運行時,
    在系統中就會存在 50 個該動態庫的數據段,
    每減少一個整型的全局變量,理論上,它將節省 4B x 50 = 200B ;
    假如能省出一個頁面,則總能省出 4KB X 50 = 200 KB 的物理內存。

下面是一些常用的優化數據段的方法:

  1. 儘可能減少全局變量 和 靜態變量。
    可以使用 nm 來列出所有在 .data 和 .bss 節的變量,方便檢查。
    查看 .data 節數據: nm --format=sysv youlib | grep -w .data
    查看 .bss 節數據: nm --format=sysv youlib | grep -w .bss

  2. 對於非內置類型的全局變量,儘可能使用全局對象指針來代替。
    進程在進入main 函數之前,運行所有非內置類型的全局變量的構造函數。
    一方面會降低進程的啓動速度,另一方面,即使沒有使用該全局變量,其也已經開始佔用物理內存,造成浪費。

例:
優化前 ==========>

class Myclass;
Myclass obj;

int main(){
	......
}

優化後 ==========>

class Myclass;
Myclass * pobj;

Myclass * getMyobj()
{
	if(pobj == NULL)
		return pobj;
}

int main()
{
	......
}

優化後的好處在於,全局對象obj 改爲一個對象指針,其不需要運行構造函數,
優化前對象obj 是在 main 函數之前,就構造出來,
優化後是在main 函數調之後,用到對象時才構造出來。
如該全局對象不是啓動的進候就需要,那麼這種優化方工可以起到節省內存的目的。

  1. 將只讀的全局變量,加上const,從而使其從數據段轉移到代碼段,利用代碼段是系統共享的特性達到節省內存使用的目的。
    優化前: int num = 10;
    優化後: const num = 10;

    但是對於非內置類型的變量,即使你使用 const 也不能將其轉移到 .rodata 段,因爲其要運行構造函數,有可能對其成員變量賦值。對於 C++ 來講,const 對象只是表明該對象構造完成之後,不能再進行修改了。
    例如:可以看出,加const 與否是沒有任何效果的。
    在這裏插入圖片描述

  2. 關開字符串數組的優化
    例如: static const char * errstr[]={"message for err", "message for err2", "message for something"};

    優化方法1:
    static const char errstr[][21]={"message for err", "message for err2", "message for something"}

    優化方法2:

static const char msgstr[]="message for err\0"
							"message for err2\0"
							"message for something\0";
static const size_t msgidx[]={
					0,
					sizeof("message for err"),
					sizeof("message for err2"),
					sizeof("message for something") };
const char * errstr(int nr){
	return msgstr + msgidx[nr];
}

優化方法3:

static const char * getErrString(int id){
	switch(id){
		case 0: return "message for err"; break;
		case 1: return "message for err2"; break;
		case 2: return "message for something"; break;
		default: return ""; 
	}
}

數據段優化的主要思路是 將數據從數據段 移到 代碼段,利用代碼段在系統內共享的特點不節省內存使用。


1.3.1.5.5 重寫的符號

編寫程序時有一個宗旨: 不要在頭文件中定義變量。

編譯單元: 當一個C 或 CPP 文件在編譯時,預處理器首先遞歸包含頭文件,形成一個含有所有必要信息的單個源文件,這個源文件就是一個編譯單元。這個編譯單元會被編譯成爲一個與 C 或 CPP 文件同名的目標文件(.o 或 .obj)。鏈接程序把不同編譯單元中產生的符號聯繫起來,構成一個可執行程序。

對於每一個變量,在編譯時都有一個鏈接屬性:內部鏈接 和 外部鏈接。
內部鏈接:該變量只是在當前的編譯單元有效,在同一個編譯單元中,不允計有同名的變量。
外部鏈接:該變量不只侷限於當前的編譯單元,在所有編譯單元中生效。一個變量在定義時,如果沒有限定符(如static),缺省爲外部鏈接。

在同一編譯單元中,同一標識符不應該同時具有內部鏈接和外部鏈接兩種聲明。具備外部鏈接的標識符,應該只定義一次。

const 屬性,在GCC 和 G++ 中含義有些不同
在GCC 中,使用 const 修飾的變量,具有外部鏈接屬性;
而在 G++ 中,使用const 修飾的變量,則是內部鏈接屬性,只在當前編譯單元生效。

static 屬性,不論是在GCC 還是 G++ 中,都是內部鏈接。

總結:

  1. 對於普通的全局變量來講,其定義應該放在源程序(分配空間)中,在頭文件中應該使用extern 聲明該變量(只聲明,不分配空間)。這樣多個編譯單元用到該全局變量時,將使用的是同一地址。
  2. 對於 const 限定的全局變量,放在頭文件中。
    使用 GCC 進行編譯時,該全局變量將具備外部鏈接性性。如果在多個編譯單元中引用,將報錯。
    使用 G++ 進行編譯時,該全局變量將具備內部鏈接屬性,如果在多個單元中使用,剛編譯器將創建多個同名但地址不同的全局變量。
  3. 對於 static 限定的全局變量,放在頭文件中,該全局變量將具備內部鏈接屬性,如果在多個編譯單元中使用,則編譯器將創建多個同名但地址不同的全局變量。

1.3.1.6 代碼段

代碼段在整個系統內共享,而且內存不足時還能回收。因此,實際上代碼段對系統的內存使用影響不大,不是優化的重點。

1.3.1.6.1 readelf -a xxx

要想優化代碼段,先通過readelf 來查看其代碼段都包含哪些內容。

# readelf -a /bin/ls

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4049a0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          124728 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       00000000000000c0  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           0000000000400358  00000358
       0000000000000cd8  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000401030  00001030
       00000000000005dc  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000040160c  0000160c
       0000000000000112  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000401720  00001720
       0000000000000070  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000401790  00001790
       00000000000000a8  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000401838  00001838
       0000000000000a80  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         00000000004022b8  000022b8
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004022e0  000022e0
       0000000000000710  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         00000000004029f0  000029f0
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         0000000000402a00  00002a00
       0000000000011259  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000413c5c  00013c5c
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000413c80  00013c80
       0000000000006974  0000000000000000   A       0     0     32
  [17] .eh_frame_hdr     PROGBITS         000000000041a5f4  0001a5f4
       0000000000000804  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         000000000041adf8  0001adf8
       0000000000002c6c  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       000000000061de00  0001de00
       0000000000000008  0000000000000000  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       000000000061de08  0001de08
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .jcr              PROGBITS         000000000061de10  0001de10
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .dynamic          DYNAMIC          000000000061de18  0001de18
       00000000000001e0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         000000000061dff8  0001dff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         000000000061e000  0001e000
       0000000000000398  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         000000000061e3a0  0001e3a0
       0000000000000260  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           000000000061e600  0001e600
       0000000000000d68  0000000000000000  WA       0     0     32
  [27] .gnu_debuglink    PROGBITS         0000000000000000  0001e600
       0000000000000034  0000000000000000           0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  0001e634
       0000000000000102  0000000000000000           0     0     1

可以看到 ,代碼段包括如下Section:
.interp.note.ABI-tag.note.gnu.build-i.gnu.hash
.dynsym.dynstr.gnu.version.gnu.version_r
.rela.dyn.rela.plt.init.plt.plt.got.text
.fini.rodata.eh_frame_hdr.eh_frame

如果想縮減代碼段的話,可以從縮減這些section 入手。

主要方法如下:

1.3.1.6.2 在編譯執行文件時,不要使用 “-export-dynamic”

在缺省情況下,主程序不會導出其內部定義的函數2和變量名。
如果你想導出,在編譯時加上 “-Wl, -export-dynamic”,這會增加代碼段和數據段的大小,佔據更多的內存。

在這裏插入圖片描述


1.3.1.6.3 優化 .rodata 節

.rodata 主要存放一些常量,前面提到優化數據段的時候,建議在一些不會修改的全局變量前加上const ,將其移到 .rodata 節,利用代碼段系統共享的特性來節約內存。
將 const 常量修改爲宏定義,使其作爲立即數編譯到代碼中。


1.3.1.6.4 優化text 節
text 節主要包括了編譯後的執行指令,介紹兩種方法來減小編譯後的指令代碼。
(1) 刪除冗餘代碼

由於冗餘代碼的存在,有可能會使原本可以在一個物理頁面保存的代碼,卻要使用兩個物理頁面。可以說,冗餘代碼使有效代碼變得分散而導致代碼段使用的物理內存增加。

另外,冗餘代碼可能會增加頁面故障的數量,從而導致進程運行效率下降。

可以使用 GCC 的 “–Wunused” 和 “–Wunreachable-code” 來檢測冗餘代碼,顯示warning 信息。
–Wunused 是 --Wunused-function、–Wunused-label、–Wunused-variable、–Wunused-value 選項集合。
–Wunused-parameter 需單獨使用。

–Wunused-function 用來警告存在一個未使用的static 函數定義或 存在一個只聲明卻未定義的static 函數。
–Wunused-label 用來警告存在一個使用了卻未定義或者存在一個定義了卻未使用的label。
–Wunused-variable 用來警告存在一個定義了卻未使用的局部變量或者非常量static 變量。
–Wunused-value 用來警告一個顯式計算表達式的結果不被使用。
–Wunused-parameter 用來警告一個函數的參數在函數的實現中並未被用到。
–Wunreachable-code 用來警告代碼中有不可到達的代碼。

(2) 使用Thumb 指令

減小編譯後生成的代碼段尺寸的一個重要方法是使用Thumb 指令。
爲兼容數據總線寬度爲16的應用系統,ARM 體系結構除了支持執行效率很高的32位 ARM 指令集外,同時支持 16位的Thumb 指令集。
Thumb 指令集是ARM 指令集的一個子集,允許指令編碼爲16位的長序。
與等價的32位代碼相比較,Thumb 指令集在保留32代碼優勢的同時,大大節省了系統的存儲空間。

在一般情況下,Thumb 指令與ARM 指令的時間效率和空間效率關係爲:

  • Thumb 代碼所需的存儲空間約爲 ARM 代碼的 60% ~ 70%
  • Thumb 代碼使用的指令數比ARM 代碼多約 30% ~ 40%
  • 若使用32位的存儲器,ARM 代碼約比 Thumb 代碼快40%
  • 若使用16位的存儲器,Thumb 代碼比ARM 代碼快約40% ~ 50%
  • 與ARM 代碼比較,使用Thumb 代碼,存儲器的功耗 會降低30%
  1. Thubm 指令的編譯
    gcc -o xxx -mthumb xxx.c

  2. ARM 程序 和 Thubm 程序混合使用
    如果使用thumb 和arm 混合編程時,必須加上“–mthumb-interwork” 選項,如:
    gcc -o a1.o -c -mthumb -mthumb-interwork a1.c
    gcc -o a2.o -c a2.c
    gcc -o hello a1.o a2.o
    一般來說:

    • 強調速度的場合,應該使用ARM 程序,且在 32位的內存中運行,儘可能提高運行速度。
    • 有一些功能只有 ARM 程序能夠完成,如禁止異常中斷
    • 當處理器進入異常中斷的處理程序時,程序狀態自動切換到ARM 狀態。
    • ARM 處理器總是從 ARM 狀態開始執行。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章