Linux perf獲得性能計數器

上一次講了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);

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章