【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 状态开始执行。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章