上一次講了DVFS,但是論文中都是根據PMC計算功耗。仿真中PMC容易獲得,但是實際的系統中,我們很難獲得,得從linux內核源碼層次訪問寄存器。
簡單的我們可以使用perf_event_open()這個子系統來獲得,雖然不太方便吧,還是可以用的。
大致上講,perf_event_open()有兩個使用模式,一個叫做計數,一個叫做採樣。所謂計數,就是測量一段時間內某個事件發生的次數,比如獲取每1秒內運行的指令數。所謂採樣,就是在某個時間點查看某個狀態,比如我想每1秒記錄下當前的IP寄存器的值。從編程的角度看,計數模式要容易理解地多,也容易實現地多。所以這篇博客先講怎麼使用perf的計數模式。
最簡單的計數模式就是隻監測一個計數器。比如我每一秒獲取剛剛過去的那一秒內的指令數(instructions)。複雜的計數模式就是同時監測多個計數器。比如我每一秒獲取剛剛過去的那一秒內的指令數(instructions)、時鐘週期數(cycles)、分支指令數(branch instructions)等等。接下來就先講單一計數器模式,而後講多計數器模式。
舉一個簡單的例子:
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/perf_event.h>
//目前perf_event_open在glibc中沒有封裝,需要手工封裝一下
int perf_event_open(struct perf_event_attr *attr,pid_t pid,int cpu,int group_fd,unsigned long flags)
{
return syscall(__NR_perf_event_open,attr,pid,cpu,group_fd,flags);
}
int main()
{
struct perf_event_attr attr;
memset(&attr,0,sizeof(struct perf_event_attr));
attr.size=sizeof(struct perf_event_attr);
//監測硬件
attr.type=PERF_TYPE_HARDWARE;
//監測指令數
attr.config=PERF_COUNT_HW_INSTRUCTIONS;
//初始狀態爲禁用
attr.disabled=1;
//創建perf文件描述符,其中pid=0,cpu=-1表示監測當前進程,不論運行在那個cpu上
int fd=perf_event_open(&attr,0,-1,-1,0);
if(fd<0)
{
perror("Cannot open perf fd!");
return 1;
}
//啓用(開始計數)
ioctl(fd,PERF_EVENT_IOC_ENABLE,0);
while(1)
{
uint64_t instructions;
//讀取最新的計數值
read(fd,&instructions,sizeof(instructions));
printf("instructions=%ld\n",instructions);
sleep(1);
}
}
在編譯後可以直接運行,會輸出instructions值。如果需要得到每秒的值,除了得到的數據相減之外,我們還可以每次將計數器清0。代碼如下
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/perf_event.h>
//目前perf_event_open在glibc中沒有封裝,需要手工封裝一下
int perf_event_open(struct perf_event_attr *attr,pid_t pid,int cpu,int group_fd,unsigned long flags)
{
return syscall(__NR_perf_event_open,attr,pid,cpu,group_fd,flags);
}
int main()
{
struct perf_event_attr attr;
memset(&attr,0,sizeof(struct perf_event_attr));
attr.size=sizeof(struct perf_event_attr);
//監測硬件
attr.type=PERF_TYPE_HARDWARE;
//監測指令數
attr.config=PERF_COUNT_HW_INSTRUCTIONS;
//初始狀態爲禁用
attr.disabled=1;
//創建perf文件描述符,其中pid=0,cpu=-1表示監測當前進程,不論運行在那個cpu上
int fd=perf_event_open(&attr,0,-1,-1,0);
if(fd<0)
{
perror("Cannot open perf fd!");
return 1;
}
//啓用(開始計數)
ioctl(fd,PERF_EVENT_IOC_ENABLE,0);
while(1)
{
uint64_t instructions;
//讀取最新的計數值
read(fd,&instructions,sizeof(instructions));
//讀取後清零
ioctl(fd,PERF_EVENT_IOC_RESET,0);
printf("instructions=%ld\n",instructions);
sleep(1);
}
}
除了監控指令數,還有很多可以,具體可以看官方手冊
如果你說多個計數器簡單,創建多個perf文件描述符,然後每個都用read去讀嘛。額,確實可以!但是呢,當監測的事件很多,而且讀取頻率很高時,那麼read()調用的開銷就不可再忽略了。那麼如果能夠把多個計數器的值通過一次read()調用獲取,性能上能夠提高不少。perf提供了“組”的概念,這也是多計數器perf的核心內容。第一個perf fd還是和本來一樣的方法創建(除了要多設置一個read_format),而後面的perf_event_open()中的參數group_fd就傳入第一個perf fd。這樣他們就成爲了一個組,並且用第一個perf fd代表整個組。下面代碼演示瞭如何同時監測指令數和時鐘週期數。
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/perf_event.h>
//目前perf_event_open在glibc中沒有封裝,需要手工封裝一下
int perf_event_open(struct perf_event_attr *attr,pid_t pid,int cpu,int group_fd,unsigned long flags)
{
return syscall(__NR_perf_event_open,attr,pid,cpu,group_fd,flags);
}
//每次read()得到的結構體
struct read_format
{
//計數器數量(爲2)
uint64_t nr;
//兩個計數器的值
uint64_t values[2];
};
int main()
{
struct perf_event_attr attr;
memset(&attr,0,sizeof(struct perf_event_attr));
attr.size=sizeof(struct perf_event_attr);
//監測硬件
attr.type=PERF_TYPE_HARDWARE;
//監測指令數
attr.config=PERF_COUNT_HW_INSTRUCTIONS;
//初始狀態爲禁用
attr.disabled=1;
//每次讀取一個組
attr.read_format=PERF_FORMAT_GROUP;
//創建perf文件描述符,其中pid=0,cpu=-1表示監測當前進程,不論運行在那個cpu上
int fd=perf_event_open(&attr,0,-1,-1,0);
if(fd<0)
{
perror("Cannot open perf fd!");
return 1;
}
//接下來創建第二個計數器
memset(&attr,0,sizeof(struct perf_event_attr));
attr.size=sizeof(struct perf_event_attr);
//監測
attr.type=PERF_TYPE_HARDWARE;
//監測時鐘週期數
attr.config=PERF_COUNT_HW_CPU_CYCLES;
//初始狀態爲禁用
attr.disabled=1;
//創建perf文件描述符
int fd2=perf_event_open(&attr,0,-1,fd,0);
if(fd2<0)
{
perror("Cannot open perf fd2!");
return 1;
}
//啓用(開始計數),注意PERF_IOC_FLAG_GROUP標誌
ioctl(fd,PERF_EVENT_IOC_ENABLE,PERF_IOC_FLAG_GROUP);
while(1)
{
struct read_format aread;
//讀取最新的計數值,每次讀取一個結構體
read(fd,&aread,sizeof(struct read_format));
printf("instructions=%ld,cycles=%ld\n",aread.values[0],aread.values[1]);
sleep(1);
}
}
可以注意到,最大的變化就是數據的讀取。當使用了“組”的形式之後,那麼每次read()就是讀取一個特定的結構體。這個結構體struct read_format不是固定的,會根據組內計數器數量和struct perf_event_attr的read_format字段的設置而變化。上面的代碼用了最簡單的方法,把struct perf_event_attr的read_format設置爲PERF_FORMAT_GROUP,那麼每次讀取的結構體其實就是1+nr個64位整數,其中第一個整數nr就是計數器數量,後面nr個整數就是每一個計數器的值,順序和加入組的順序相同。
第二大變化就是ioctl()的第三個參數由0變爲了PERF_IOC_FLAG_GROUP。這個標誌表明操作是對組進行的,可以理解爲kernel幫我們把ioctl依次作用在了組的每一個計數器上。所以呢,如果每次讀取後,要把組內所有計數器都清空,需要使用:
ioctl(fd,PERF_EVENT_IOC_RESET,PERF_IOC_FLAG_GROUP);