一般会用到的valgrind的命令:
1下面是检测内存方面问题的,常用:
Valgrind ./main(换成program名称) (缺省为—tool=memcheck)
valgrind --tool=memcheck --trace-children=yes(检查子进程) --leak-check=full ./main(换成program名称)
Leak是指,存在一块没有被引用的内存空间,或没有被释放的内存空间,如summary,只反馈一些总结信息,告诉你有多少个malloc,多少个free 等;如果是full将输出所有的leaks,也就是定位到某一个malloc/free。
valgrind --tool=memcheck --leak-resolution=high ./main(换成program名称)
valgrind --tool=memcheck --show-reachable=yes ./main(换成program名称)
valgrind --tool=memcheck --show-reachable=yes --log-file=proglog(这里是你希望要保存的log日志名称) ./main(换成program名称)
在运行Valgrind时加上--show-reachable=yes参数,可以找到每一个未来匹配的free或new,输出结果和上面差不多,不过显示了更多未释放的内存。
2下面是检测其它方面的
valgrind --tool=callgrind ./main
valgrind --tool=cachegrind ./main
valgrind --tool=helgrind ./main ( 检查多线程情况 )
valgrind --tool=massif ./main
之前提到过内存泄漏问题,我们有memcheck工具来检查。很爽。但是有时候memcheck工具查了没泄漏,程序一跑,内存还是狂飙。这又是什么问题。。。
其实memcheck检查的内存泄漏只是狭义的内存泄漏,或者说是严格的内存泄漏,也就是说,在程序运行的生命周期内,这部分内存你是彻底释放不了了,即,你失去了这些地址。
其实还有第二种类型的内存泄漏,就是,长期闲置的内存堆积。这部分内存虽然你还保存了地址,想要释放的时候还是能释放。关键就是你忘了释放。。。杯具啊。这种问题memcheck就不给力了。这就引出了第二个内存工具valgrind –tool=massif。
会产生massif.out.****的文件,然后使用
ms_print massif.out.*** >massif.log
|
打开
Valgrind是GNU计划在Linux下的一个产品,通过模拟CPU内核来对程序的性能进行测试。许多GNU的软件包括KDE、GNOME和计算化学软件如CP2K(CP动力学),Zori(量子Monte Carlo计算)等都利用它进行性能改进。网络上存在不少Valgrind的教程,但是几乎都是讲用memcheck工具来测试内存泄露或数组越界的,很少有讲cachegrind的,即使有也是直接翻译的英文手册。因此,这里我分享一些本人用valgrind测试cache和分支的经验。
1 缓存性能背景
缓存cache对科学计算程序的性能有着至关重要的影响。
计算机运行的指令和数据都位于内存中。从现在的观点来看,内存的速度相对于CPU简直就是蜗牛与喷气式飞机的差别。看这段代码(C):
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv)
{
int i, sum;
int a[5] = {1, 2, 3, 4, 5};
sum = 0;
for(i = 0; i < 5; i++) sum += a[i]; // Cycle
return 0;
}
Cycle的执行为:
movl $0, -4(%rbp) ;sum 赋值为0
movl $0, -8(%rbp) ;i 赋值为0
jmp .L2
.L3:
movl -8(%rbp), %eax ;取 i 值
cltq ;预取指令
movl -32(%rbp,%rax,4), %eax ;形成 a[i] 地址并取值
addl %eax, -4(%rbp) ;sum += a[i]
addl $1, -8(%rbp) ; i++
.L2:
cmpl $4, -8(%rbp) ;i < 5 ?编译器用的实际是 i <=4 的判据
jle .L3
对于瓶颈的cycle代码, 本来呢,从流水线的角度,CPU可以同时执行取 a[i] 指令(movl -32(%rbp,%rax,4), %eax )和 i++ (addl $1, -8(%rbp))指令,然后同时执行求和(addl %eax, -4(%rbp))和比较(cmpl $4, -8(%rbp)指令。可是,由于涉及到内存定位的指令movl -32(%rbp,%rax,4), %eax具有极高的延迟,CPU必须等待才能执行求和命令,使得大量流水线时钟被浪费掉。
幸好,现代处理器提供了缓存Cache功能。缓存是CPU上面的硬件,一般CPU具有两级缓存:L1和L2。前者小而快,后者大而慢。比如我的一台微机(Debian 5 操作系统):
GenuineIntel L1 16KB 4unit
L2 3MB 18unit
内存 8GB >800unit
可见,缓存的速度比内存要快得多,因此,提高缓存利用率对提高计算速度十分重要。
一般而言,缓存对编程者是透明的,也就是说:编程者无法手动指定某些变量必须存在于缓存中,必须完全靠计算机硬件来实现。我们只能间接地使它载入缓存。当然,某些硬件提供了一些汇编指令实现软件预取,如_mm_XX指令。另外,某种情况下,比如编写一些硬件驱动程序,我们希望数据不经过缓存而直接读写到物理内存中,这需要声明该内存区域uncacheable,不过在科学计算程序中,这种技巧似乎没有用,就不提了。
缓存建立在局部性原理上。比如,当程序访问 a[0] 时候,CPU先找cache,如果没有(miss),它就会在内存中寻找,然后把 a[0] 及其附近的内存的数据如 a[1] a[2] a[3] a[4] 及其后面的某些东西一次性载入cache。这样,下次循环时,如果CPU需要 a[1],那么cache中已经有了(hit),那么就省去了访问内存的步骤,从而大大提升了速度。
注意,cache的读取是有一定对齐要求的。比如 a[0] 地址为 66(十进制),而cache为64B对齐的,则cache会一次性读入64~127地址的数据。
2 最大化缓存利用率
我们利用一个经典例子:矩阵乘法来说明这个问题。这是用FORTRAN写的:
!Naive
subroutine dgemm(X, Y, Z, N)
implicit none
integer N
real(kind = 8) X(N,N)
real(kind = 8) Y(N,N)
real(kind = 8) Z(N,N)
integer i, j, k
do j = 1, N
do i = 1, N
Z(i,j) = 0
do k = 1, N
Z(i,j) = Z(i,j)+X(i,k)*Y(k,j)
enddo
enddo
enddo
return
end subroutine
这是最简单的写法。把它变一下形式:
!Wisdom
subroutine dgemm(X, Y, Z, N)
implicit none
integer N
real(kind = 8) X(N,N)
real(kind = 8) Y(N,N)
real(kind = 8) Z(N,N)
integer i, j, k
do j = 1, N
do i = 1, N
Z(i,j) = 0
enddo
do k = 1, N
do i = 1, N
Z(i,j) = Z(i,j)+X(i,k)*Y(k,j)
enddo
enddo
enddo
return
end subroutine
编译后,发现wisdom的code比naive快的多。分析一下原因。由于FORTRAN是按列存储的,在naive代码中,Z(i,j) = Z(i,j)+X(i,k)*Y(k,j)在 k 循环中,X(i,k)与Y(k,j)会不停的发生缓存冲突,而X(i,k)本身又是低缓存命中率(k是远指标);在wisdom中,Z(i,j) = Z(i,j)+X(i,k)*Y(k,j)在 i 循环中,变动的只有前指标,X(i,k)和Z(i,j)的命中率较高,且两者可以较好的同时并入缓存中,因此,他的速度要远远快于naive代码。
3 valgrind测试缓存性能
矩阵乘法可以用valgrind测试。假设程序叫dgemm,则执行:
valgrind --tool=cachegrind ./dgemm
注意,这个程序执行可能会非常慢,因为valgrind类似一个虚拟机。
对上面两段代码,输出为:
naive代码:
==6242== I refs: 6,248,685,388
==6242== I1 misses: 867
==6242== L2i misses: 835
==6242== I1 miss rate: 0.00%
==6242== L2i miss rate: 0.00%
==6242==
==6242== D refs: 3,448,886,481 (3,310,598,005 rd + 138,288,476 wr)
==6242== D1 misses: 136,333,827 ( 134,824,798 rd + 1,509,029 wr)
==6242== L2d misses: 1,077,386 ( 2,263 rd + 1,075,123 wr)
==6242== D1 miss rate: 3.9% ( 4.0% + 1.0% )
==6242== L2d miss rate: 0.0% ( 0.0% + 0.7% )
==6242==
==6242== L2 refs: 136,334,694 ( 134,825,665 rd + 1,509,029 wr)
==6242== L2 misses: 1,078,221 ( 3,098 rd + 1,075,123 wr)
==6242== L2 miss rate: 0.0% ( 0.0% + 0.7% )
wisdom代码:
==6204== I refs: 14,903,957,102
==6204== I1 misses: 867
==6204== L2i misses: 835
==6204== I1 miss rate: 0.00%
==6204== L2i miss rate: 0.00%
==6204==
==6204== D refs: 8,218,095,831 (7,902,970,313 rd + 315,125,518 wr)
==6204== D1 misses: 41,929,490 ( 40,409,341 rd + 1,520,149 wr)
==6204== L2d misses: 1,077,386 ( 2,263 rd + 1,075,123 wr)
==6204== D1 miss rate: 0.5% ( 0.5% + 0.4% )
==6204== L2d miss rate: 0.0% ( 0.0% + 0.3% )
==6204==
==6204== L2 refs: 41,930,357 ( 40,410,208 rd + 1,520,149 wr)
==6204== L2 misses: 1,078,221 ( 3,098 rd + 1,075,123 wr)
==6204== L2 miss rate: 0.0% ( 0.0% + 0.3% )
这里,I表示指令数,I1,L2i分别表示一级指令缓存和二级指令缓存;D1,L2d分别表示一级数据缓存和二级数据缓存。这些意义很简单,如,D refs表示读缓存次数,D1 misses表示要寻找的变量不在缓存中的次数,D1 miss rate自然是失误率,如40,409,341/7,902,970,313=0.511%。
可见,指令缓存不成问题,而在一级数据缓存中,wisdom方法的失误率仅为0.5%,而naive高达3.9%!高的L1 cache miss rate可以使速度大大的降低!
在这个程序中,我们事先知道只有dgemm 的子过程为瓶颈。如果程序中有大量函数,则这个总数值可能没有意义,没关系,我们可以看输出文件,如cachegrind.out.xxx. 里面有每个函数的执行信息:
fl=/home/coolrainbow/code/cache_test/dgemm.f90
fn=MAIN__
fn=dgemm_
1 89 6 0 36 1 0 27 3 0
20 532 1 0 229 0 0 2 0 0
21 1064532 0 0 456304 68 0 152 0 0
22 1672000 1 0 760000 0 0 152000 19000 0
24 1059765 1 0 454261 0 0 152 0 0
25 2119532058 0 0 908522202 124807 0 302640 0 0
26 12710836908 3 0 6960696402 40281369 0 302638974 0 0
dgemm_为函数名,1,20,21等表示指令在源代码中的行数,后面是该行指令的refs,之类的参数。这样,我们就可以找到程序的缓存瓶颈而加以改进。 当然,对于C语言程序,如果你的函数是内联的话,最好暂时禁用内联,否则柔在别的函数里就无法分辨了。
分支预测背景
对于下面一段代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i, flag;
i = 0;
if(i > 5)
flag = 1;
else
flag = 0;
return 0;
}
汇编后,对于if后面,得到:
movl $0, -12(%rbp) ;i = 0
cmpl $5, -12(%rbp) ;i > 5?
jle .L2
movl $1, -8(%rbp) ;flag = 1
jmp .L3
.L2:
movl $0, -8(%rbp) ;flag = 0
.L3:
movl $0, %eax
我们知道,现代CPU具有流水线结构,在执行i = 0这个指令(movl $0, -12(%rbp))时,可以把后面的指令进行预取,如(cmpl $5, -12(%rbp),jle .L2),但是,究竟之后是执行 movl $1, -8(%rbp)还是movl $0, -8(%rbp)呢?这只有在 i 的值知道后才能确定。CPU内部有个硬件,叫做分支缓冲区(BTB),它会预测下一条应该执行哪些指令,比如预测movl $1, -8(%rbp),那么这条指令就会进入CPU。如果下一条真的是这条指令,当然皆大欢喜,可是如果不是的话,那么这条指令就得扔掉,重新载入movl $0, -8(%rbp)这条指令。可是代价就是浪费了机器的时钟周期。
分支语句是计算中最麻烦的语句之一,他会大大降低程序的执行效率,但是,如果这个分支具有一定的规律性,以现代CPU的水平,常常可以准确的预测下一步的指令,那么这时if语句的破坏性就较小。如:
// predictable
for(i = 0; i < N; i++)
if( (i & 1) == 0)
flag = 1;
else
flag = 0;
就是说,i为偶数时,flag = 1,奇数则flag = 0。这个分支的规律性很强,现代CPU可以准确的预测出来。但是:
// unpredictable
srand(time(NULL));
for(i = 0; i < N; i++)
if( i > rand()%(N+1))
flag = 1;
else
flag = 0;
就是说,i大于当时生成的某个随机数时,flag=1,否则0。这个分支完全是随机的,很难预测。
6 valgrind测试指令分支
非常简单,只要下面的命令:
valgrind --tool=cachegrind --branch-sim=yes ./branch
对上面的两端代码执行后,可以发现:
Predictable:
==16023== Branches: 217,376 (217,206 cond + 170 ind)
==16023== Mispredicts: 2,034 ( 2,007 cond + 27 ind)
==16023== Mispred rate: 0.9% ( 0.9% + 15.8% )
Unpredictable:
==16019== Branches: 1,119,364 (1,019,186 cond + 100,178 ind)
==16019== Mispredicts: 33,744 ( 33,710 cond + 34 ind)
==16019== Mispred rate: 3.0% ( 3.3% + 0.0% )
很明显,第二段代码的预测错误率3%远大于第一段代码0.9%。当然,这个演示的例子无所谓。一般,如果错误率>10%,往往就有必要考虑优化这个分支语句。因为,好的程序中,if的预测准确性都能在95%以上。
优化方法可以是,把不易预测的分支变成容易预测的,或者干脆去掉分支,这样可以使用SIMD指令。当然说的容易,这常常需要高超的技巧。