Linux C/C++調試之五:程序運行耗時的組成

分析程序出現的啓動緩慢、響應緩慢和操作卡頓等性能問題時,第一步不該是打開代碼編輯器瀏覽我們的代碼,而是首先確定問題是否發生在我們的代碼中,簡單點的方法就是打開top做一個大致的判斷,看一看各CPU的us、sy、id耗時的佔比都是多少。進一步來講,我們需要確定時間都花到什麼地方了,是我們的代碼中?內核中?還是進程睡大覺的地方?現在假設我們的程序運行在一個CPU資源豐富的環境中,因此就不考慮進程爭奪CPU的問題了。我們來看一下,程序運行耗時的組成成分,以及相關的一些需要注意的地方。

1. 牆上時間、用戶CPU時間和系統CPU時間

牆上時間就是程序運行所耗的現實世界中的時間,用戶CPU時間則是CPU花在執行用戶代碼所耗的時間,系統CPU時間則是CPU花在執行內核代碼所耗的時間,使用time(1)命令即可分析這3類時間:

$ time grep -nr "test"

real    0m8.528s
user    0m1.107s
sys     0m0.698s

用戶CPU時間就是CPU在執行我們寫的用戶進程代碼所耗的時間,不包括花在系統調用的時間;系統CPU時間就是當執行流通過系統調用,進入內核空間後,CPU執行內核代碼所耗的時間,不包括陷入休眠等待的時間。

高系統CPU時間佔比

系統CPU時間佔比高不見得就是內核的問題,很可能是用戶空間代碼的問題,例如下面的例子:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    const char *BAD_FILE = "/proc/uptime";

    while(true)
    {
        open(BAD_FILE, O_WRONLY);
    }

    return 0;
}

當執行一段時間後,強制退出程序:

$ time ./badopen
^C

real    0m10.907s
user    0m1.831s
sys     0m9.071s

10s的牆上時間有9s花在了系統CPU時間上,並不是內核代碼有問題,而是因爲用戶代碼重複執行會出錯的系統調用。

當然了,高系統CPU時間佔比確實可能是內核代碼的問題,我就遇到過一次因爲驅動的問題,而導致程序卡死在了一個ioctl調用中。這樣的問題使用perf會比較容易定位,perf是個非常強大的內核分析工具,有時間的話我會寫一些相關的文章。

系統CPU時間不是系統調用開始到系統調用結束的時間

許多系統調用都會導致進程休眠,進程休眠的時間並不算在系統CPU時間中。

$ time sleep 1

real    0m1.003s
user    0m0.001s
sys     0m0.003s

用戶CPU時間和系統CPU時間可能大於牆上時間

多線程程序很容易發生這樣的情況:

#include <thread>

void work() {
    int a = 0;
    for (int i = 0; i < 10000000; i++) {
        a += i;
    }
}

int main() {
    std::thread t0([](){ work(); });
    std::thread t1([](){ work(); });

    t0.join();
    t1.join();

    return 0;
}
$ g++ -std=c++11 multithread.cpp -o multithread -lpthread
$ time ./multithread 

real    0m0.045s
user    0m0.085s
sys     0m0.000s

小結

用戶CPU時間也好,系統CPU時間也好,統計的都是CPU處於忙碌狀態的時間。然而性能問題並不都是由於CPU過於忙碌導致的,還有可能是因爲過多的陷入休眠狀態,GUI編程的一條“金科玉律”:不要在UI線程中執行阻塞式IO和sleep操作,就是爲了避免這種情況導致的卡頓問題。因此,本節中的兩類時間是不足以將程序運行耗時劃分完畢的,還需要考慮進程睡眠的時間。

2.系統調用延遲(syscall latency)

“系統調用延遲”這個詞的出處是strace的手冊,指的是系統開始到結束的時間差,這個時間差會包括了系統調用系統CPU時間和進程休眠的時間。例如前面提到過的sleep程序,如果使用strace分析時,加上-w參數,就會統計系統調用延遲:

$ strace -cw sleep 1
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.79    1.000504     1000504         1           nanosleep
  0.08    0.000818         818         1           execve
  0.04    0.000409          82         5           close
  0.02    0.000198          33         6           mmap
  0.01    0.000141          47         3           brk
  0.01    0.000139          35         4           mprotect
  0.01    0.000126          42         3           openat
  0.01    0.000105          35         3         3 access
  0.01    0.000078          78         1           munmap
  0.01    0.000070          23         3           fstat
  0.00    0.000030          30         1           read
  0.00    0.000018          18         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    1.002636                    32         3 total

不加-w參數,則只統計系統CPU時間:

$ strace -c sleep 1
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         1           read
  0.00    0.000000           0         5           close
  0.00    0.000000           0         3           fstat
  0.00    0.000000           0         6           mmap
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         3         3 access
  0.00    0.000000           0         1           nanosleep
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         3           openat
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                    32         3 total

需要注意的是,系統CPU時間並不是系統調用延遲的子集,系統CPU時間還包括一些處理異常的耗時,並不包含在系統調用的耗時中。

3.缺頁異常耗時

用戶CPU時間、系統CPU時間和系統調用休眠時間是否就是程序執行耗時的全部組成成分呢?答案仍然是否定的,請看下面的代碼:

// bigdata.cpp
#include <stdio.h>

const int ARRAY_SIZE = 200 * 1024 * 1024;
const int array[ARRAY_SIZE] = {1};

int main()
{
    int total = 0;
    for (int i = 0; i < ARRAY_SIZE; i += 4096)
    {
        total += array[i];
    }
    printf("all data is loaded\n");
    return 0;
}

使用time統計(統計前使用echo 3 > /proc/sys/vm/drop_caches清除磁盤緩存):

$ time ./bigdata
all data is loaded

real    0m16.963s
user    0m0.033s
sys     0m1.286s

再使用strace統計系統調用延遲(同樣統計前使用echo 3 > /proc/sys/vm/drop_caches清除磁盤緩存):

$ strace -cw ./bigdata
all data is loaded
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 95.77    0.045840       45840         1           execve
  1.11    0.000532         177         3           brk
  0.76    0.000366         122         3         3 access
  0.67    0.000322          64         5           mmap
  0.51    0.000246          82         3           fstat
  0.30    0.000143          36         4           mprotect
  0.24    0.000113         113         1           munmap
  0.22    0.000105          53         2           openat
  0.17    0.000079          79         1           write
  0.14    0.000066          33         2           close
  0.06    0.000029          29         1           read
  0.05    0.000022          22         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.047863                    27         3 total

即使把用戶CPU時間0.033s、系統CPU時間1.286s和系統調用延遲0.047863s相加(系統CPU時間和系統調用延遲有些重合的地方),也離16.963s的牆上時間相差很多。問題出在了哪裏呢?問題出在了缺頁異常上。

bigdata.cpp會生成一個大約800M的可執行文件,數據段佔據了其中大部分體積。當我們遍歷array數組的每個元素時,需要將這800M的數據段全部讀取一遍,由此產生的缺頁異常耗時就無法忽略了。缺頁異常耗時大部分集中在等待磁盤IO時的睡眠上,這部分睡眠時間無法被統計在系統調用延遲上,因爲確實沒有任何系統調用。mmap系統調用不會導致磁盤IO,它很快就返回了,等到我們在用戶空間代碼讀寫被映射區域時,產生了缺頁異常,進而發生了磁盤IO消耗了時間。

其他

在用戶CPU時間、系統CPU時間、系統調用睡眠時間和缺頁異常耗時之外,還有其他組成,比如說CPU處理中斷或異常的耗時。CPU處理中斷或異常時進程被迫進入睡眠狀態,所以中斷或異常來得很頻繁時也可能會導致性能問題(上面提到的缺頁異常也是異常的一種,但是相比其他異常,缺頁異常更容易導致性能問題,所以我單列出來了)。

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