博客 / 詳情

返回

[Linux] 手寫輕量C++函數性能探查器:CPU佔用率&耗時

平時在寫C++程序優化性能的時候,經常想知道某些熱點函數跑起來到底佔用了多少CPU,花了多少時間。Linux中有很多性能探查工具,諸如perf、top等等,但大多數時候只想要測量某個函數或者代碼塊,用不着特別龐大的工具。查閲一些資料後,筆者寫了兩個輕量簡單的探查器,分別探查代碼塊的CPU佔用率和耗時,記錄分享一下。

統計CPU佔用率

核心思路

計算某個函數的CPU佔用率,可以粗略地理解為計算某個函數佔用CPU的時間與CPU在所有進程上花費的時間的比值,這就要求我們要拿到CPU的詳細統計信息。Linux在/proc中記錄了現成的CPU統計數據。其中/proc/stat中記錄了系統啓動到現在,CPU在不同“狀態”上累計花的時間;proc/self/stat中則包含了當前進程的CPU各項統計數據。讀取這兩個文件不需要root權限,因此實現起來不會很麻煩。我們需要做的就是解析這兩個文件,提取所需的參數即可。

解析/proc/stat

在終端中執行命令cat /proc/stat,可以看到返回結果的第一行通常如下:

cpu  518127 71 120189 7077551 8165 0 48610 0 0 0

這一行的含義是:從系統啓動到現在,CPU在不同“狀態”上累計花了多少時間。單位是USER_HZ(大多數機器上可以粗略理解成1/100秒,但準確值建議用sysconf(_SC_CLK_TCK)獲取)。各列分別對應着不同的字段,字段順序從左到右一般是這些:

  1. user:用户態時間;
  2. nice:用户態時間(跑在低優先級進程上花的時間);
  3. system:內核態時間(系統調用、內核代碼執行的時間);
  4. idle:空閒時間;
  5. iowait:等待I/O的時間;
  6. irq:處理硬中斷的時間;
  7. softirq:處理軟中斷的時間;
  8. steal:虛擬化相關的“被偷走的時間”(在虛擬機環境裏,CPU去跑別的系統/別的虛擬機了,就像被偷走了一樣);
  9. guest:跑guest虛擬CPU的時間(虛擬化場景);
  10. guest_nice:跑“nice過的guest”的時間;

其中注意guestguest_nice不少內核/工具口徑裏會和user/nice存在重複計入的關係,所以最好將其排除。要計算CPU從系統啓動到現在花費的所有時間,把這些值求和即可,計算單位均是USER_HZ

解析/proc/self/stat

終端中執行命令cat /proc/self/stat,能看到如下的返回結果:

76723 (cat) R 52152 76723 52152 34816 76723 4194304 95 0 0 0 0 0 0 0 20 0 1 0 1515316 9162752 418 18446744073709551615 94507585687552 94507585710729 140728218500048 0 0 0 0 0 0 0 0 0 17 1 0 0 0 0 0 94507585726960 94507585728704 94508453613568 140728218505341 140728218505361 140728218505361 140728218509291 0

這裏面有非常多字段,但是我們只需要關心下面幾個字段:

  1. utime (14):進程在用户態被調度運行的累計時間;
  2. stime (15):進程在內核態被調度運行的累計時間;
  3. cutime (16):已等待的子進程累計用户態時間;
  4. cstime (17):已等待的子進程累計內核態時間;

如果只關心當前進程,那麼cutimecstime是不需要考慮的。由於我實現的是輕量版本,就跳過前面的字段,只考慮utimestime,對其求和即可,單位也都是USER_HZ

求差計算

/proc/stat/proc/self/stat獲取的值都是“從開機/啓動到現在”的累計計數,所以要測“某段時間內”的佔用,就需要在代碼塊的開頭和結尾做兩次採樣取差值:

  • procDelta = procEnd - procStart
  • totalDelta = totalEnd - totalStart
  • cpuPercent = procDelta / totalDelta * 100

這樣求得的cpuPrecent就是代碼塊的CPU佔用率了。

代碼實現

摸清楚了原理,代碼實現就不難了。筆者把這部分邏輯抽成CpuProfiler類,完整實現如下:

#include <fstream>
#include <string>
#include <chrono>

class CpuProfiler {
public:
    // 開始測量時記錄當前進程和系統CPU時間
    void start() {
        lastProcTime = getProcessCpuTime();
        lastTotalTime = getTotalCpuTime();
    }

    // 結束測量時再次讀取時間並計算CPU佔用率(百分比)
    double stop() {
        unsigned long procTime = getProcessCpuTime();
        unsigned long totalTime = getTotalCpuTime();
        unsigned long procDiff = procTime - lastProcTime;
        unsigned long totalDiff = totalTime - lastTotalTime;
        // 計算佔用率百分比
        double cpuPercent = 0.0;
        if (totalDiff != 0) {
            cpuPercent = (double)procDiff / totalDiff * 100.0;
        }
        return cpuPercent;
    }

private:
    unsigned long lastProcTime = 0;
    unsigned long lastTotalTime = 0;

    // 獲取當前進程的 CPU 時間(用户態+內核態),單位:時鐘節拍
    unsigned long getProcessCpuTime() {
        std::ifstream statFile("/proc/self/stat");
        if (!statFile.is_open()) {
            return 0;
        }

        // 按順序讀取stat文件中的字段
        int pid;
        char comm[256], state;
        statFile >> pid;             // 進程ID
        statFile.ignore(256, ')');   // 跳過括號內的進程名稱
        statFile.ignore(1);          // 略過空格
        statFile >> state;           // 進程狀態(R/S等)
        // 跳過不關心的項,一直到第13列結束
        long dummy;
        for (int i = 0; i < 10; ++i) {
            statFile >> dummy;
        }

        // 讀取第14列utime和第15列stime
        unsigned long utimeTicks = 0, stimeTicks = 0;
        statFile >> utimeTicks >> stimeTicks;
        return utimeTicks + stimeTicks;
    }

    // 獲取系統總的 CPU 時間(所有CPU核心累積),單位:時鐘節拍
    unsigned long getTotalCpuTime() {
        std::ifstream statFile("/proc/stat");
        if (!statFile.is_open()) {
            return 0;
        }

        std::string cpuLabel;
        unsigned long user=0, nice=0, system=0, idle=0;
        unsigned long iowait=0, irq=0, softirq=0, steal=0;

        /* 讀取第一行,如 "cpu  <user> <nice> <system> <idle> <iowait> <irq> <softirq> <steal> ..." */
        statFile >> cpuLabel 
                 >> user >> nice >> system >> idle 
                 >> iowait >> irq >> softirq >> steal;

        // 注意:後續還有 guest 等字段,這裏略過
        unsigned long totalJiffies = user + nice + system + idle 
                                   + iowait + irq + softirq + steal;
        return totalJiffies;
    }
};

如果要對其再作優化,可以維護全局的文件句柄,使用時按需讀取;同時文件解析的邏輯也可以自己實現,這裏不多作贅述。

統計耗時

代碼執行耗時的測量就簡單得多了。在C++11及以後的標準中,標準庫已經提供了<chrono>時間庫,可以方便地獲取高精度的時間點。這部分的實現原理就不多介紹了,直接上代碼:

#include <chrono>
#include <cstdint>

class ElapsedProfiler {
public:
    void start() {
        m_running = true;
        m_start = Clock::now();
    }

    // 返回毫秒
    double stopMs() {
        if (!m_running) {
            return 0.0;
        }
        auto end = Clock::now();
        m_running = false;
        std::chrono::duration<double, std::milli> ms = end - m_start;
        return ms.count();
    }

    bool running() const {
        return m_running;
    }

private:
    using Clock = std::chrono::high_resolution_clock;
    bool m_running = false;
    Clock::time_point m_start{ };
};

這裏我使用了std::chrono::high_resolution_clock取得高精度的時間,其實如果為了測量毫秒級的函數耗時,沒有必要使用特別高精度的時鐘,這裏僅供參考。

使用例程

不妨編寫分別寫一個計算密集的函數和一個掛起等待的函數,驗證一下耗時和CPU佔用率的計算是否準確。代碼如下:

static void demo_cpu_heavy() {
    printf("=== 計算密集 ===\n");

    CpuProfiler cpu;
    ElapsedProfiler wall;

    cpu.start();
    wall.start();

    volatile uint64_t acc = 0;
    for (uint64_t i = 1; i <= 200000000ULL; ++i) {
        acc += (i * 2654435761ULL) ^ (acc >> 3);
    }

    const double elapsedMs = wall.stopMs();
    const auto r = cpu.stop();

    printf("acc=%llu\n", (unsigned long long)acc);
    printf("elapsed=%.3f ms, cpu=%.2f%%\n", elapsedMs, r);
}

static void demo_sleep() {
    printf("\n=== 掛起延時 ===\n");

    CpuProfiler cpu;
    ElapsedProfiler wall;

    cpu.start();
    wall.start();

    std::this_thread::sleep_for(std::chrono::milliseconds(800));

    const double elapsedMs = wall.stopMs();
    const auto r = cpu.stop();

    printf("elapsed=%.3f ms, cpu=%.2f%%\n", elapsedMs, r);
}

int main() {
    demo_cpu_heavy();
    demo_sleep();
    return 0;
}

程序輸出如下:

筆者電腦是六核十二線程,計算密集函數CPU佔用率~8%,説明基本能跑滿單線程;掛起延時的函數耗時接近800 ms,且CPU佔用率幾乎為零,説明結果符合預期,可以放心使用。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.