龙芯2E平台程序性能分析

来源:互联网 发布:命若琴弦知乎 编辑:程序博客网 时间:2024/04/30 01:19

当前版本: 0.1
完成日期: 2007-3-27
作者: Dajie Tan <jiankemeng@gmail.com>


1. 分析一段代码的性能,最常用的方法是测量这段代码的运行时间。假如我要分析下面这两段代码的性能差异,可以在代码前后插入2个变量,分别记录运行前后的时间,相减即可:

代码一:


int matrix[2047][7];


int main()
{
    int i, j, sum = 0;

    unsigned int beg, end;

A1:
    for(i = 0; i < 7; i++)
        for(j = 0; j < 2047; j++)
            sum += matrix[j][i] * 1024;

B1:

}


代码二:

int matrix[2047][7];

int main()
{
    int i, j, sum = 0;
    unsigned int beg, end;

A2:
    for(i = 0; i < 2047; i++)
        for(j = 0; j < 7; j++)
            sum += matrix[i][j] * 1024;
B2:

}

则可以在A1,A2处插入 beg = times(NULL); // 返回以过去某点为基准点的时钟滴答数
B1, B2处插入 end = times(NULL);     // 需要包含头文件 sys/times.h

相减即可得到该段代码的运行时间。

当 前linux系统的每秒经过的时钟滴答数可以使用sysconf(_SC_CLK_TCK)获得,我在linux-2.6.18 for loongson2e的环境下获取的值为 100,即最小测量的精度是10ms。这个精度要远远高于time() 1s的精度,但是这个精度还是无法满足上面测量要求,因为分别编译运行后,两段代码的运行时间是一致的,这个结果是不能被接收的,从定性的分析开看,代码 二的性能肯定是优于代码一的。



2.
使用龙芯2E系统协处理器(CP0)中的两个性能计数器(performance counter)

龙 芯2E之系统协处理器中引入了两个性能计数器,对应CP0寄存器的25号,长度为64位,低32位作为Counter 0,高32位作为Counter 1。其中CP0的24号寄存器作为2个计数器的控制寄存器,亦是64位,可以设置它,分别控制两个计数器所记录的事件。

CP0的24号寄存器简单描述:3~0 位作为使能位目前置1;第5位作为中断使能位目前置0;8~5为Counter 0的计数事件,12~9 作为Counter 1的计数事件,其余位置0。

比 如我要控制 Counter 0对时钟周期进行计数,Counter 1对一级数据缓存不命中进行计数,可以将CP0D的24号寄存器置值为 0x80f(01000 0000 1111),即Counter 0的计数事件为0000,Counter 1的计数事件为0100,这个值可以查看龙芯2E用户手册。



3. 设置性能计数器的计数事件

Counter 0常用计数事件(4位):

0000: 时钟周期计数
0001: 分支指令计数
0100: 一级I-cache不命中计数
1001: 从主存中读计数
1110: TLB重填例外计数


Counter 1常用计数事件(4位):

0001: 分支预测失败计数
0100: 一级D-cache不命中计数
0111: 访问未缓存计数
1001: 写到主存计数
1100: ITLB不命中计数

目前的内核起起来后,CP0的24号寄存器为 0x1ef,故而我要重设24号寄存器的值 0x80f(01000 0000 1111,Counter 0的计数事件为0000,Counter 1的计数事件为0100).

对龙芯2E系统协处理器的寄存器执行写操作在用户空间是没有写权限的。我们可以写一个简单的模块,把设置的操作放在模块初试化函数中来完成:

[set_event.c]

#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("GPL");

static int set_init(void)
{
    unsigned long op = 0x80f;
    asm volatile("mtc0 %0, $24"::"r"(op));
    printk(KERN_ALERT "cp0_24 set complete/n");
    return 0;
}

static void set_exit(void)
{
    unsigned int val = 0;
    asm volatile("mfc0 %0, $24":"=r"(val));
    printk(KERN_EMERG "current cp0_24 value is 0x%x/n", val);
}

module_init(set_init);
module_exit(set_exit);


相应的Makefile为:

ifeq ($(KERNELRELEASE),)

    KERNELDIR ?= /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)

modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

.PHONY: modules modules_install clean

else
    # called from kernel build system: just declare what our modules are
    obj-m := set_event.o
endif



4. 解决问题

在使用上述模块编译、安装(insmod),设置好两个计数器的计数事件后,我们依然在A1,A2插入代码,读取两个计数器的值,并记录:

  unsigned int cache_miss_beg, cache_miss_end;

  asm volatile
    (
        ".set mips3/n/t"
        "dmfc0 %0, $25/n/t"
        "dsra   %0, 32/n/t"
        :"=r"(cache_miss_beg)         //记录运行前Counter 1的值,L1 D-cache的事件
    );
  asm volatile("mfc0 %0, $25":"=r"(beg)); //记录运行前Counter 0的值,CPU时钟周期事件


在B1,B2插入:

  asm volatile("mfc0 %0, $25":"=r"(end)); //记录运行后Counter 0的值,CPU时钟周期事件
  asm volatile
    (
        ".set mips3/n/t"
        "dmfc0 %0, $25/n/t"
        "dsra   %0, 32/n/t"
        :"=r"(cache_miss_end)         //记录运行后Counter 1的值,L1 D-cache的事件
    );

  printf("Total CPU Cycles is: %d/n", end - beg);
  printf("The number of L1 D-Cache miss is: %d/n", cache_miss_end - cache_miss_beg);


基于计数器的测量方法,精度很高,可以得到程序执行期间所经过的时钟周期数。但是影响其准确性的因素也很多,主要有因进程上下文切换,致使其他进程占用处理器,影响计数器的计数,这个可以通过在低负载的机器上多次运行求平均值的方法来弱化。

如上面的例子我们执行 init 1 进入单用户模式下,此时系统仅有一个bash进程,分别执行5次求得平均值:

代码一所需时钟周期为:         1035200
代码一一级数据cache缺失次数为:   2431

代码二所需时钟周期为:         735310
代码二一级数据cache缺失次数为:   2092

代码一所需时钟周期是代码二的 1.4 倍
代码一一级数据cache缺失次数是代码二的 1.16 倍

可以看到,代码一访问数组按行访问(C语言中,数组存放于内存是按列存放),这样CPU对数据的访问就会呈“跳跃”的形态,违背了空间局部性,致使cache命中率下降,影响了整个代码的性能。



测试代码: http://people.openrays.org/~comcat/misc/pmc.tar.gz



原创粉丝点击