评测一个程序的性能有多种方法,对目前大多数benchmark,程序执行时间是一个很重要的标准。但大多数情况下时间仅仅能给出一个结果,却给不了更多的信息,如cache miss, CPU功能部件的利用率,访存指令,分支预测等。这些信息对于知道一个程序的优化往往会起到很重要的作用。
很多现代的CPU都有一些内置的计数器用来记录这些事件,X86环境下可以调用一些库对这些计数器进行设置和访问,具体可以参考这里。关于X86下使用performance counter的方法很多,而且资料也很容易找到,大家google一下就好了。
龙芯从早一个版本(loongson 2e)就提供了性能计数器,龙芯2f的计数器基本同2e大概上没有什么区别,定义了两个性能计数器(performance counter),分别映射到CP0(协处理器)的第24、25号寄存器。25号寄存器是64位,其高32位用作counter1, 对应的event1, 低32位用作counter0, 对应event0, event0和event1是由24号寄存器的5-12位控制的,其中5-8位为event0,9-12位为event1,用户可以通过设置24号寄存器相应的位来控制counter记录的事件,event0和event1对应的事件如下所示:
event0:
0000 周期
0001 分支指令
0010 指令
0011 指令并且域rs=31
0100 一级I-cache 缺失
0101 Alu1 操作已发射
0110 Mem 操作已发射
0111 Falu1 操作已发射
1000 猜测指令
1001 从主存中读
1010 固定发射队列满的次数
1011 重排队列满的次数
1100 CP0 队列满的次数
1101 Tlb 重填例外
1110 例外
1111 内部例外
event1:
0000 提交操作
0001 分支预测失败
0010 预测失败
0011 JR 且rs=31 预测失败
0100 一级D-cache 缺失
0101 Alu2 操作已发射
0110 Falu2 操作已发射
0111 Uncached 访问
1000 BHT 猜测错误
1001 写到主存
1010 浮点指针队列满的次数
1011 分支队列满的次数
1100 Itlb 缺失
1101 例外总数
1110 投机缺失
1111 队列向前加载
要使用计数器,首先要设置24号寄存器中event0和event1所对应的域。比如你要测一级I-Cache miss和一级D-Cache miss,那么就要把24号寄存器设置成0x44f(0-4位以及其他位为默认值)。但是有一个问题,龙芯2f协处理器只有在内核态下才能更改,因此我们要更改24号寄存器,需要模块的帮助。
龙芯的协处理器是通过MFC0/DMFC0来读和通过MFT0/DMFT0来写的,如果在用户态下使用MFT0/DMFT0指令,会在运行时得到“非法指 令”的错误信息。即便是在内核态下,[loongson_counter.c]
#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int event_init(void) { unsigned int event = 0x44f; asm volatile("mtc0 %0, $24"::"r"(event)); return 0; } static void event_exit(void) { } module_init(set_init); module_exit(set_exit); |
[makefile]
ifneq ($(KERNELRELEASE),) obj-m :=loongson_counter.o else KERNELDIR ?=/lib/modules/$(shell uname -r)/build PWD :=$(shell pwd) clean: rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif |
make生成模块后,使用insmod loongson_counter.ko将模块加载,这时就可以使用25号寄存器记录I-cache miss和D-cache miss了。
在具体程序中,如果你要计算一个函数带来的cache miss,可以在这个函数开头通过MFC0/DMFC0指令读取计数器中的初始值,然后在函数结尾,再读取一次,两次相减就得到了该函数造成的cache miss。注意硬件性能计数器是一个全局的计数器,即如果你在运行你要测试的函数同时运行了其他程序,其他程序造成的cache miss也会记录在计数器里。所以在保持cpu独占的情况下可以测试几次取平均值。下面以一个sort()函数为例示例怎么在函数中应用:
[sort.c]
void sort(){ usigned int icache_init, dcache_initi, cache_miss, dcache_miss; asm volatile(".set mips3\n\t" "dmfc0 %0, $25\n\t" "dsra %0, 32\n\t" "mfc0 %0, $25\n\t" :"=r"(dcache_init),"=r"(icache_init) ); //把counter的初始值记录下来 qsort(........); ...........; asm volatile(".set mips3\n\t" "dmfc0 %0, $25\n\t" "dsra %0, 32\n\t" "mfc0 %0, $25\n\t" :"=r"(dcache_miss),"=r"(icache_miss) ); //记录函数运行后计数器的值 fprintf(stderr, "I cache miss is:%d\n", icache_miss - icache_init); fprintf(stderr, "D cache miss is:%d\n", dcache_miss - dcache_init); } |
现在我们已经可以使龙芯2f的计数器起作用了,但是还存在一个问题,如果我有一个代码量相对比较多的程序,想做一个工具来多方面的评测这个程序,许要使用 到各种不同的计数器事件,而又不想每次都重新编译加载一个内核来改变这个事件,这时候就需要写一个相对稍复杂一些的模块用来设置事件,下面是我写的一个模 块,可以通过write系统调用来改变事件。
[loongson_counter.c]
#include <linux/init.h> #include <linux/modules.h> #include <linux/fs.h> #include <linux/cdev.h> MODULE_LICENSE("Dual BSD/GPL"); MODULE_AUTHOR("sponge"); MODULE_DESCRIPTION("change event of performance counter"); MODULE_ALIAS("set_event"); //function convert char to int int convert_event(char *array){ int ret = 0; int i; for(i = 0; i<=3; i++){ //printk("ret is :%x, i is %x, array[i] is %c\n", ret, i , array[i]); if(array[i]=='1') ret = ret + (1<<(3-i)); else if(array[i]=='0') continue; else return -EINVAL; } return ret; } //function used to modify the $24 register int event_write(struct file *filp, const char __user *buff, size_t count, loff_t *f_pos){ int e0, e1; //variables hold the event number char buff_k[9]; //buffer holds date transform from user space char event0[5]; char event1[5];//these two array hold event number in form of char int i; //loop counter int regval;// the value that will transform to $24 if(count != 9) return -EINVAL; //copy user date if(copy_from_user(buff_k, buff, 9)) return -EFAULT; //convert user date to array for(i = 0; i<=3; i++){ event0[i] = buff_k[i]; event1[i] = buff_k[i+4]; } //convert char to int if((e0 = convert_event(event0))==-EINVAL) return -EINVAL; if((e1 = convert_event(event1))==-EINVAL) return -EINVAL; //printk("event0 is %x, event1 is %x\n",e0,e1); //compute $24 regval = (e0<<5)|(e1<<9)|0xf; asm volatile("mtc0 %0, $24"::"r"(regval)); //unsigned int val = 0; //asm volatile("mfc0 %0, $24":"=r"(val)); //printk(KERN_EMERG "current cp0_24 value is 0x%x\n", val); return 0; } struct cdev event; struct file_operations event_ops; //init function static int init_event(void){ //register the device event if(register_chrdev_region(MKDEV(33,0),1,"pcounter")){ return -EFAULT; } //register operations event_ops.write = event_write; cdev_init(&event, &event_ops); cdev_add(&event,MKDEV(33,0),1); return 0; } //clean function static void exit_event(void){ //unregister device cdev_del(&event); unregister_chrdev_region(MKDEV(33,0),1); } module_init(init_event); module_exit(exit_event); |
Makefile跟上一个例子的基本一致。
使用该模块的方法为:
1、在root下使用命令 mknod /dev/pcounter c 33 0
2、在root下使用命令 chmod 666 /dev/pcounter
3、编译该模块,使用insmod 加载模块
4、在程序中可以通过write系统调用来更改性能计数器的事件,代码如下:
#define writelong 9 int fd; fd=open("/dev/pcounter, O_RDWR"); write(fd, "01000100", writelong); //写入的8个字符,前4位表示event0, 后4位表示event1 |
Good!Fighting!