客户端做到一定程度,都会做启动优化,启动优化主要有如下方式
客户端性启动性能优化比起服务器要简单,服务器动不动就搞缓存、分布式、算法调优、客户端启动优化说来说去,核心思想不过下几点
涉及到的工具
用了这些方法和工具后,完全可以忽略掉微软的prefetcher,这个工具1不开源,其次资料非常少,能起到的作用也有限,微软只会用闭源这一招,但是比他强大的方法实在太多。
重点讲下 基址固定,dll文件预读,二进制relink,二进制文件磁盘整理
每个DLL和可执行文件都有一个首选基地址。它表示该模块被映射到进程地址空间时最佳的内存地址。在构建可执行文件时,默认情况下链接器会将它的首选基地址设为0x400000。对于DLL来说,链接器会将它的首选基地址设为0x10000000,然后将该地址以及代码、数据的相关地址都写入它们的PE文件中。当它们被加载时,加载程序读取首选基地址的值,并试图把它们加载到相应位置。
对于可执行文件和DLL中的代码,它们运行的时候所引用的的数据的地址,在链接的时候就已经确定。并且这些都是当exe文件或是DLL被加载到它们的首选基地址处时才是有效的。
对于汇编代码:MOV [0x00414540] ,5
它是将5赋值给0x00414540处的内存地址。此地址已经固定。它说明当EXE或是DLL被加载到0x400000处时,0x00414540才是正确的。然而并不是所有模块都会被载入到首选基地址处。一旦模块没有被加载到首选基地址处,在该模块中对于地址的引用都是错误的,这时候基址重定位就是必须的。
所谓重定位就是当某模块未被载入到首选基地址时,加载器会计算模块实际载入的地址跟首选基地址的差值,将这个差值加到机器指令所引用的原来的地址,得到的就是模块中各指令所引用的数据在本进程地址空间的正确地址,随后加载器会修正模块中对每个内存地址的引用。
为了便于系统有能力对各数据的地址进行修正,windows提供了重定位段。它包含很多基址重定位信息。这个段是有很多项组成。每一项表示一个要重定位的地址,它包含一个字节偏移量列。该偏移量表示一条机器指令所使用的内存地址。这便于系统在确认该模块需要重定位时对需要重定位的数据进行定位。
当链接器在构建模块时,它会将重定位段嵌入到生成的文件中。
对exe文件来说,由于每个文件总是使用独立的地址空间,所以exe文件总是可以被加载到首选基地址上。对于DLL来说,很多DLL都是使用寄主进程的地址空间,不能保证各DLL的首选基地址各不相同。
所以DLL一定要有重定位段,除非在链接时使用/FIXED 开关。此时链接器会去掉重定位段。如果此后模块未被载入首选基地址,由于无法重定位,模块不会被载入,导致程序无法运行。
如果加载程序将模块加载到它的首选基地址,那么系统就不会访问模块的重定位段。否则系统会打开模块的重定位段并遍历其中的条目,对每个条目,加载程序先找到包含机器指令的那个存储页面,算出差值,将差值加到机器指令当前正在使用的内存地址上。
对于上例:假如它是在一DLL中。如果该DLL实际被加载到0x20000000处,由于首选基地址是0x10000000,两者差为0x10000000。加载器修正之后,该指令所引用的实际地址变成了0x10414540。这样对各个数据的引用都会引用到正确的地址 。
要知道基址重定位需要很大的系统开销:
1:加载程序需要遍历模块重定位段并修改模块中大量的代码。这增加了程序初始化时间。
2:当加载程序写入模块的代码页面时,由于它们具有写时复制属性,写时复制机制会导致系统从页交换文件中分配空间来容纳这些修改后的页面。
明白了这些我们应该清楚,为每个映射到进程的DLL设置不同的首选基地址是必须的。在链接时,选中ProjectProperties\Linker\Advancved,然后在BaseAddress中设置。这听起来很容易,但是如果程序需要载入大量的模块,我们还需要这样一个一个设置吗?话又说回来了,有些模块并不是我们创建的,我们应该怎么办呢?
Windows为我们提供了一个名为:Rebase.exe的工具。
如果在执行它时为它提供一组映像文件名,它会执行以下操作:
1:它会模拟创建一个地址空间。
2:它会打开这一组映像文件,并得到它们的大小和首选基地址。
3:它会在模拟的地址空间对模块重定位的过程进行模拟,以便各模块没有交叉。
4:对于每个需要重定位的模块,它会解析模块的重定位段,并修改模块在磁盘文件中的代码。
5:将每个模块新的首选基地址写入各个模块磁盘文件中。
所以推荐你在构建完所有项目后运行Rebase.exe。
由于Microsoft在发布windows之前,已经使用Rebase.exe对所有操作系统提供的模块进行了重定位,所以即使将操作系统的所有模块都映射到进程地址空间也不会发生交叉的现象。我们不再需要对随操作系统一起发布的模块进行基址重定位。
使用Rebase.exe或是手工修改各模块的首选基地址,以免某些模块不被加载到首选基地址上。这当然可以提高系统性能。但是我们还可以更显著的提高性能。
这种技术就是模块绑定。
让我们来回顾一下程序加载模块的过程:
加载程序首先为进程创建虚拟地址空间,接着把可执行文件映射进来。之后打开可执行文件的导入段,将该程序需要的DLL进行定位并把它们也载入进来。随着加载程序将DLL模块加载到进程地址空间,它会同时检查每个DLL的导入段。如果一个DLL有导入段,那么加载程序会继续将所需的额外的DLL模块映射到进程地址空间。
当所有DLL都已被载入进程地址空间,它开始修复所有对导入符号的引用。这时它会再次查看每个模块的导入段,对导入段中每个符号,它会检查对应DLL的导出段,然后取出该符号的RVA并给他加上DLL模块被载入到进程地址空间的虚拟地址。计算出符号在进程的虚拟地址(VA),写入到可执行模块的导入段中。这样当一个符号被引用的时候,程序会查看可执行模块的导入段并取得导入符号虚拟地址(VA)。
这就是模块加载的大概过程。可以看到,每次在程序加载时,都要将导入段的符号地址从其他DLL中获得,然后写入到导入段的相应位置(IAT)。这十分耗费时间。
所谓的模块绑定就是说在运行之前,所有导入符号在进程地址空间的地址已经获得,不需要加载时在计算出来这节省初始化时间,另外将导入符号的虚拟地址写入exe模块的导入段,也会由于写时复制机制将要修改的页面以系统页交换文件为后备存储器。这会遇到与基址重定位相似的问题。所以模块绑定对提高系统性能的提高是显著的。
Visual Studio提供了一个名为Bind.exe的工具,如果在执行它的时候传给它一个映像文件名,它会对其执行模块绑定操作。
具体过程为:
在整个过程中Bind.exe做了两个重要假设:
1:进程初始化时所需的DLL都被映射到了它们的首选基地址。
2:绑定完成之后,DLL导出段所列出的符号的位置没有发生改变。这可以通过检查每个DLL的时间戳来保证。
如果上述假设有一个不成立。加载程序必就向绑定之前一样,手动修正可执行文件导入段。如果都成立加载程序就可以不用做这些工作了。
DLL预加载是指在显示加载DLL之前,进行DLL的预读
不同系统预读方法选取对性能影响较大,经测试在Win7上用
HANDLE file_handle = CreateFileA(file_path,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
这种方式打开文件,再用::ReadFile 读,性能较好。
在WinXP上用 适合用这种方式
HMODULE dll_module = ::LoadLibraryExA(
file_path,
NULL,
LOAD_WITH_ALTERED_SEARCH_PATH | DONT_RESOLVE_DLL_REFERENCES);
完整代码如下:摘自 chrome
bool PreReadImage(const char* file_path, size_t size_to_read, size_t step_size);
typedef unsigned char uint8;
// A helper function to touch all pages in the range
// [base_addr, base_addr + length).
void TouchPagesInRange(void* base_addr, size_t length) {
if ((base_addr == NULL) || (length <=0))
return ;
// Get the system info so we know the page size. Also, make sure we use a
// non-zero value for the page size; GetSystemInfo() is hookable/patchable,
// and you never know what shenanigans someone could get up to.
SYSTEM_INFO system_info = {};
GetSystemInfo(&system_info);
if (system_info.dwPageSize == 0)
system_info.dwPageSize = 4096;
// We don't want to read outside the byte range (which could trigger an
// access violation), so let's figure out the exact locations of the first
// and final bytes we want to read.
volatile uint8* touch_ptr = reinterpret_cast
volatile uint8* final_touch_ptr = touch_ptr + length - 1;
// Read the memory in the range [touch_ptr, final_touch_ptr] with a stride
// of the system page size, to ensure that it's been paged in.
uint8 dummy;
while (touch_ptr < final_touch_ptr) {
dummy = *touch_ptr;
touch_ptr += system_info.dwPageSize;
}
dummy = *final_touch_ptr;
}
PIMAGE_DOS_HEADER GetDosHeader(HMODULE module) {
return reinterpret_cast
}
PIMAGE_NT_HEADERS GetNTHeaders(HMODULE module) {
PIMAGE_DOS_HEADER dos_header = GetDosHeader(module);
return reinterpret_cast
reinterpret_cast
}
bool VerifyMagic(HMODULE module) {
PIMAGE_DOS_HEADER dos_header = GetDosHeader(module);
if (dos_header->e_magic != IMAGE_DOS_SIGNATURE)
return false;
PIMAGE_NT_HEADERS nt_headers = GetNTHeaders(module);
if (nt_headers->Signature != IMAGE_NT_SIGNATURE)
return false;
if (nt_headers->FileHeader.SizeOfOptionalHeader !=
sizeof(IMAGE_OPTIONAL_HEADER))
return false;
if (nt_headers->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC)
return false;
return true;
}
bool PreReadImage(const char* file_path, size_t size_to_read, size_t step_size) {
if (::Util::UI::IsWin7()) {
// Vista+ branch. On these OSes, the forced reads through the DLL actually
// slows warm starts. The solution is to sequentially read file contents
// with an optional cap on total amount to read.
HANDLE file_handle = CreateFileA(file_path,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
if (file_handle == INVALID_HANDLE_VALUE)
return false;
// Default to 1MB sequential reads.
const DWORD actual_step_size = max(static_cast
static_cast
LPVOID buffer = ::VirtualAlloc(NULL,
actual_step_size,
MEM_COMMIT,
PAGE_READWRITE);
if (buffer == NULL)
return false;
DWORD len;
size_t total_read = 0;
while (::ReadFile(file_handle, buffer, actual_step_size, &len, NULL) &&
len > 0 &&
(size_to_read ? total_read < size_to_read : true)) {
total_read += static_cast
}
::VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(file_handle);
} else {
// WinXP branch. Here, reading the DLL from disk doesn't do
// what we want so instead we pull the pages into memory by loading
// the DLL and touching pages at a stride. We use the system's page
// size as the stride, ignoring the passed in step_size, to make sure
// each page in the range is touched.
HMODULE dll_module = ::LoadLibraryExA(
file_path,
NULL,
LOAD_WITH_ALTERED_SEARCH_PATH | DONT_RESOLVE_DLL_REFERENCES);
if (!dll_module)
return false;
//base::win::PEImage pe_image(dll_module);
if (!VerifyMagic(dll_module))
return false;
// We don't want to read past the end of the module (which could trigger
// an access violation), so make sure to check the image size.
PIMAGE_NT_HEADERS nt_headers = GetNTHeaders(dll_module);
size_t dll_module_length = min(
size_to_read ? size_to_read : ~0,
static_cast
// Page in then release the module.
TouchPagesInRange(dll_module, dll_module_length);
FreeLibrary(dll_module);
}
return true;
}
PE文件重组是,应用程序冷启过程中,对某个PE文件所有函数按启动过程调用顺序进行二进制重组。
重组工具:sawbuck,这是google 提供的一系列工具。
可在 https://code.google.com/p/sawbuck/wiki/SyzygyDesign 下载工具
该网页也有原理介绍。
首先,先下载sawbuck工具包
重组方法如下:
1. 先确定要重组的应用程序目录,PE文件,并要有PE文件对应的PDB文件。如:
要对chrome.dll进行重组,则还需要准备chrome.dll.pdb.
2. 把 需重组的 PE文件,和对应的PDB,剪切到一个文件夹下,此文件夹一般与应用程序的EXE文件在同一目录。保证应用程序当前目录下, 没有要重组的PE文件和对应的PDB.
如:把chrome.dll 和 chrome.dll.pdb 移到original 目录下
3. 用sawbuck工具包中的 instrument.exe ,对chrome.dll 进行hook.
instrument.exe 是命令行工具,使用如下命令可以对chrome.dll 进行hook,
Instrument.exe --input-image=D:\Release\original\chrome.dll --output-iamge=D:\Release\chrome.dll --mode=calltrace
并产生hook后同名的chrom.dll和chrome.dll.pdb.
4. 手动复制call_trace_client.dll(在sawbuck工具包中) 到刚产出chrome.dll 相同的目录如:
5. 启动call_trace_service.exe ,命令如下:
start call_trace_service.exe start --verbose --trace-dir=traces
--trace-dir 指定call_trace_service.exe 的产出目录,这里是目录traces, 与call_trace_service.exe 在同一目录。
6. 运行chrome.exe
7. 让chrome.exe 运行一短时间后,执行如下命令:
8. call_trace_service.exe stop 停止记录数据。
call_trace_service.exe停止后会在traces目录产生一些 bin文件如:
有几个进程调用要重组的PE 文件,就会产生几个bin文件,通过进程id可以区分不同的进程产生的bin. 针对本例子chrome,找到主进程的bin文件,
如trace-chrome.exe-20130927185447-8400.bin
9. 用reorder.exe 对bin文件进行处理。
reorder.exe --instrumented-image=***** --output-file=***** --pretty-print traces\*****
产出
10. relink.exe --input-image=**** --output-image=****** --order-file=***
到这里已经产生出重组后的PE文件,和对应的 PDB
命令总结:
1.instrument.exe --input-image=** --output-iamge=** --mode=calltrace
2.start call_trace_service.exe start --verbose --trace-dir=**
3.runing 应用程序
4.call_trace_service.exe stop
5.reorder.exe --instrumented-image=*** --output-file=*** --pretty-print *
6.relink.exe --input-image=*** --output-image=**** --order-file=*****
注意:
1. 前一步的输出一般都是下一步的输入 ,其中任何一步失败都会到重组失败
2. 并不是对任意的PE文件relink后都有很大的性能提升,要先分析PE文件的缺页情况,一般可以用xperf工具来分析
预读dll相差的时间,有的时候会很大,经过调查和分析,跟文件在磁盘上的碎片有关系。如果一个文件在磁盘上碎片比较多,磁头需要seek就会比较多,而磁头每次seek非常消耗时间。现在市面上的磁盘大约需要15-20ms。工具wcontig,可以查看单个文件的碎片分布并且可以整理单个文件的磁盘碎片。文件在磁盘碎片越多,预读也会越耗时。
让文件连续的分布,在安装过程中部产生碎片,有2中方式
1、 修改安装包的解压接口,在文件解压的时候,保证连续的写到硬盘上
2、 采用开源工具,在安装完后,用此工具对安装后的文件目录进行一次碎片整理