分析程序出現的啓動緩慢、響應緩慢和操作卡頓等性能問題時,第一步不該是打開代碼編輯器瀏覽我們的代碼,而是首先確定問題是否發生在我們的代碼中,簡單點的方法就是打開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處理中斷或異常時進程被迫進入睡眠狀態,所以中斷或異常來得很頻繁時也可能會導致性能問題(上面提到的缺頁異常也是異常的一種,但是相比其他異常,缺頁異常更容易導致性能問題,所以我單列出來了)。