减少共享内存页
正如Mach-O可执行格式概述中所述,Mach-O二进制文件的__DATA段中的数据是可写的,因此是可共享的(通过写时复制)。在内存不足的情况下,可写数据会增加可能需要写入磁盘的页数,从而降低分页性能。对于frameworks,可写数据最初是共享的,但有可能被复制到每个进程的内存空间。
减少可执行文件中动态或非常量数据(non-constant)的数量会对性能产生重大影响(significant),特别是对于frameworks。以下章节将向您展示如何减少可执行文件的__DATA
段的大小,从而减少共享内存页面的数量。
将Data声明为const
使__DATA
段更小的最简单方法是将全局作用域(globally scoped data)的数据标记为常量。大多数时候,很容易将数据标记为常量。例如,如果你永远不打算修改数组中的元素,你应该在数组声明中包含const
关键字,如下所示:
1 | const int fibonacci_table[8] = {1, 1, 2, 3, 5, 8, 13, 21}; |
记住将指针标记为常量(在适当的时候)。在下面的例子中,字符串”a”和”b”是常量,但数组指针foo不是常量:
1 | static const char *foo[] = {"a", "b"}; |
要将整个声明标记为常量,需要向指针添加const
关键字以使指针成为常量。在下面的例子中,数组和它的内容都是常量:
1 | static const char *const foo[] = {"a", "b"}; |
有时,您可能希望重写代码以分离出常量数据。下面的示例包含一个结构数组,其中只有一个字段声明为const。因为整个数组没有声明为const
,所以它存储在__DATA
段中。
1 | struct { |
为了将尽可能多的数据存储在__TEXT
段中,创建两个并行(parallel)数组,一个标记为常量,另一个标记为非常量:
1 | const char *const imageNames[100] = { "FooImage", /* . . . */ }; |
如果未初始化的数据项包含指针,编译器不能将该项存储在__TEXT
段中。字符串在__TEXT
段的__cstring section
中结束,但数据项的其余部分,包括指向字符串的指针,在__DATA
段的const section
中结束。在下面的例子中,daytimeTable
最终会在__TEXT
和__DATA
段之间分割,尽管它是常量:
1 | struct daytime { |
要将整个数组放在__TEXT
段中,必须重写此结构,使其使用固定大小(fixed-size)的char数组而不是字符串指针,如下例所示:
1 | struct daytime { |
不幸的是,如果字符串的大小相差很大,就没有好的解决方案,因为这种解决方案会留下大量未使用的空间。
数组被分成两个段,因为编译器总是在__TEXT
段的__cstring section
中存储常量字符串。如果编译器将数组的其余部分存储在__DATA
段的__data section
中,则字符串和指向字符串的指针可能会在不同的页面上结束。如果发生这种情况,系统将不得不用新地址更新指向字符串的指针,如果指针在__TEXT段中,则不能这样做,因为__TEXT段被标记为只读。因此指向字符串的指针以及数组的其余部分必须存储在__DATA
段的const section
中。__const section
保留给声明为const
的不能放在__TEXT
段中的数据。
初始化静态数据
正如Mach-O可执行格式概述中所指出的,编译器将未初始化的静态数据存储在__DATA段的__bss section
中,并将初始化的数据存储在__data section
中。如果你在__bss section
中只有少量的静态数据,你可能会考虑将其移动到__data section
。将数据存储在两个不同的部分中增加了可执行文件使用的内存页数,这反过来又增加了分页的可能性。
合并__bss
和__data
sections的目的是减少应用程序使用的内存页数。如果将数据移动到__data
区域会增加该区域的内存页数,则此技术没有任何好处。事实上,在__data section
中添加页面会增加在启动时读取和初始化该数据所花费的时间。
假设你声明了以下静态变量:
1 | static int x; |
要将这些变量移动到可执行文件的__data
段的__data section
,您将更改定义如下:
1 | static int x = 0; |
避免临时定义(Tentative-Definition)符号
编译器将遇到的任何重复符号放在__DATA段的__common section(参见Mach-O可执行格式概述)。这里的问题与未初始化的静态变量相同。如果一个可执行文件的非常量全局数据分布在几个sections中,则这些数据更有可能位于不同的内存页上;因此,页面可能必须分别交换进和换出(swapped in and out)。__common section
的目标与__bss section
的目标相同:如果可执行文件中有少量数据,则将其从可执行文件中删除。
暂定定义符号的一个常见来源是头文件中该符号的定义。通常,标头声明一个符号,但不包括该符号的定义;定义是在实现文件中提供的。但是出现在头文件中的定义可能导致该代码或数据出现在包含头文件的每个实现文件中。此问题的解决方案是确保头文件只包含声明,而不包含定义。
对于函数,你显然会在头文件中声明该函数的原型,并将该函数的定义放在实现文件中。对于全局变量和数据结构,应该执行类似的操作。与其在头文件中定义变量,不如在实现文件中定义它并适当地初始化它。然后,在头文件中用extern
关键字声明该变量。这种技术将变量定义本地化到一个文件,同时仍然允许从其他文件访问该变量。
当您不小心导入相同的头文件两次时,还可以获得临时定义符号。要确保不这样做,请包含预处理器指令,以禁止包含已经包含的文件。因此,在你的头文件中,你会有以下代码:
1 |
|
然后,当你想要包含这个头文件时,按照以下方式包含它:
1 |
分析Mach-O可执行文件
你可以使用几种工具来确定非常量数据占用了多少内存。这些工具可以报告数据使用的各个方面。
当你的应用程序或framework正在运行时,使用size
和pagestuff
工具来查看各种data sections有多大以及它们包含哪些符号。需要注意以下几点:
- 要查找具有大量非常量数据的可执行文件,请在__DATA段中检查具有较大__data section的文件。
- 检查__bss和__common sections中可以移除或移动到__data section的变量和符号。
- 要定位虽然声明为常量,但编译器不能将其视为常量的数据,请在__DATA段中检查带有__const section的可执行文件或目标文件。
__DATA段中一些较大的内存消费者是初始化但未声明为const
的固定大小的全局数组。有时可以通过在源代码中搜索“[]={
”来找到这些tables。
您还可以让编译器帮助您找到可以将数组设置为常量的地方。将const
放在所有您怀疑可能是只读的初始化数组前面,然后重新编译。如果一个数组不是真正的只读,它将不会编译。删除错误的const
并重试