这篇笔记的起因是我想要偷窥一下IntelMKL库中spmv的实现细节,有可能的话看是不是可以在汇编层面做一些优化的工作。需要分析的MKL库函数是:
void mkl_scsrmv (char *transa, MKL_INT *m, MKL_INT *k, float *alpha, char *matdescra, float *val, MKL_INT *indx,MKL_INT *pntrb, MKL_INT *pntre, float *x, float *beta, float *y);
最初的尝试是简单的从MKL静态库中提取相关的obj文件,通过objdump反汇编得到spmv核心的实现代码。与spmv相关的MKL静态库包括:
libmkl_core.a | 最内层的核心运算函数 |
libmkl_intel_thread.a | 中间层分支接口 |
libmkl_intel_lp64.a | 最外层运算接口 |
反汇编的大概方法:
(1) 使用 nm –s 命令导出库文件符号表
(2) 在符号表中搜索顶层函数名称(前面标记T),找出其调用的下层函数(前面标记U)
(3) 如果没有到达最内层的核心运算代码,重复进行第(2)步;否则在符号表中搜索下层函数名称,确定其所在的obj文件
(4) 使用ar –x [file] 命令从库文件中解压出obj文件
(5) 使用 objdump –d 命令反汇编obj文件,得到汇编代码
实际操作中发现这种方法想要dump出期望的核心运算函数的汇编代码非常困难,原因有两个:
1. 根据调用的数据格式和参数不同,以及具体硬件平台支持的如SSE等优化指令集的区别,最外层运算函数对应的核心运算函数的分支数量非常大,分支的命名不易弄懂,不易确定正确的调用路径;
2. 在寻找调用路径的过程中,如果仅从符号表不能确定,则需要反汇编出中间层调用部分的代码做参考,但是有些函数调用是用函数指针来实现的,从汇编代码中看不到被调用的函数符号,使得这个过程变得困难。
最终我采用的是使用gdb动态调试的方法,来确定最终实际被调用的核心运算函数。具体步骤如下:
1. 编写一个调用mkl_scsrmv的“壳程序”,并静态编译链接MKL库(使用-static编译)
2. gdb启动程序,设置“display/i $pc”(每次中断打印下一条汇编指令)
3. b main,run
4. disassemble显示汇编指令
5. 在欲考察的函数处设置断点,格式是 b *指令地址,如b *( main+offset ) 或 b *0xNNNN
6. c 执行到断点
7. si进入函数(对应源代码调试的s);ni运行下一条汇编指令(对应源代码调试的n)
8. 重复4-7,直到进入最内层的核心代码,这时可以直接拷贝disassemble的结果,也可以使用函数分支的符号,到开始导出的符号表中查找对应的obj文件,之后dump出汇编代码
确定断点的位置有一些技巧,对于函数分支的命名规律的了解能节省大量的时间;另外需要关注的指令基本只有callq一种,但是要注意callq接着的不一定是函数符号,比如使用函数指针的情况下,callq后面是寄存器名,此时要确定调用的函数分支就完全依靠gdb的动态调试了。