valgrind命令

 

一般会用到的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指令。当然说的容易,这常常需要高超的技巧。

你可能感兴趣的:(cache,测试,Integer,fortran,subroutine,硬件驱动)