1 gperftools 簡介
gperftools 是一款 Google 的開源高性能內(nèi)存相關工具集奠蹬,包括 tcmalloc 內(nèi)存管理工具做个,還有一些例如 cpu profiler、heap profiler 等性能分析工具,本系列將逐一介紹。
2 CPU profiler 簡介
CPU profiler 主要是通過采樣的的方式,給出一段時間內(nèi)程序?qū)嶋H占用cpu時間偏進行統(tǒng)計和分析刚盈,優(yōu)點是使用起來簡潔方便。
性能分析通過抽樣方法完成道媚,默認是1秒100個樣本扁掸,一個樣本是10毫秒,所以如果程序運行時間不到10ms最域,那么得到的結(jié)果可能會和開始執(zhí)行的時候不同谴分。
3 本次測試簡介
- 使用的是 ubuntu 20.04 系統(tǒng)。
- 采用直接調(diào)用提供的 API(在需要測試的代碼的前后分別調(diào)用
ProfilerStart()
和ProfilerStop()
)的方式進行測試镀脂。
4 安裝環(huán)境并測試
安裝 unwind
sudo apt install libunwind-dev
安裝 gperftools
cd ~/Download
git clone https://github.com/gperftools/gperftools.git
cd gperftools
sh autogen.sh
./configure
make all
sudo make install
編寫測試代碼牺蹄,監(jiān)控開始,參數(shù)為需要生成的文件名:
/* Start profiling and write profile info into fname, discarding any
* existing profiling data in that file.
*
* This is equivalent to calling ProfilerStartWithOptions(fname, NULL).
*/
PERFTOOLS_DLL_DECL int ProfilerStart(const char* fname);
監(jiān)控結(jié)束:
/* Stop profiling. Can be started again with ProfilerStart(), but
* the currently accumulated profiling data will be cleared.
*/
PERFTOOLS_DLL_DECL void ProfilerStop(void);
如果開啟了新的線程薄翅,需要在線程起始添加如下函數(shù)進行線程的的注冊沙兰,但測試發(fā)現(xiàn)有無該語句并不造成影響。
/* Routine for registering new threads with the profiler.
*/
PERFTOOLS_DLL_DECL void ProfilerRegisterThread(void);
使用時需要包含的頭文件:
#include <gperftools/profiler.h>
編譯測試代碼
g++ cpu_profiler_test.cpp -o cpu_profiler_test --lprofiler
執(zhí)行 cpu_profiler_test
翘魄,生成 .profile
文件鼎天。
5 無環(huán)境情況下測試
有時候并不能要求所有編譯程序的環(huán)境都安裝一遍 gperftools
和 unwind
,更多的是直接編譯運行暑竟,所以需要在編譯可執(zhí)行文件時就要準備好相關的源文件斋射。
在這個項目中本人將源碼作為 submodule,編譯時本地生成 lib 文件但荤,可以直接進行鏈接罗岖。
工程如下,使用流程詳見 README.md
文件:
https://github.com/ixiaolonglong/memory_tool/tree/master/gperftools
在編譯工程時需要添加如下編譯選項腹躁,避免被編譯器優(yōu)化:
# tcmalloc options
add_compile_options(
-fno-builtin-malloc
-fno-builtin-calloc
-fno-builtin-realloc
-fno-builtin-free)
這里記錄一下靜態(tài)庫與動態(tài)庫鏈接桑包,對于靜態(tài)庫來說,需要描述所有遞歸用到的 lib:
target_link_libraries(cpu_profiler_test PRIVATE
profiler
fake_stacktrace_scope
sysinfo
spinlock
maybe_threads
logging
unwind
pthread)
而如果是動態(tài)庫纺非,則鏈接就會簡單很多:
target_link_libraries(cpu_profiler_test PRIVATE
profiler
pthread)
https://www.zhihu.com/question/277160878
這個不是cmake的坑哑了,應該是你glog庫的坑赘方,我猜想glog庫是你自行編譯,而glog庫編譯時沒有動態(tài)鏈接gflags導致的垒手。如果你生成動態(tài)庫時蒜焊,就使用target link library生成倒信,再配合上rpath尋找路徑科贬,是可以支持a依賴b,b依賴c鳖悠,而你在a中只要寫b的依賴而不用寫c的依賴榜掌。
6 報告
執(zhí)行程序的環(huán)境不一定非要安裝 gperftools,但生成的 profile 文件時必須要安裝 gperftools 使用其 pprof 工具進行解析乘综。
需要安裝圖形工具 Graphviz
:
sudo apt-get install graphviz
生成不同類型的報告命令:
# 生成性能報告(層次調(diào)用節(jié)點有向圖)輸出到web瀏覽器顯示
pprof cpu_profiler_test cpu_test.profile --web
# 生成pdf格式的性能報告(層次調(diào)用節(jié)點有向圖)
pprof cpu_profiler_test cpu_test.profile --pdf > prof.pdf
# 生成文本格式的性能報告輸出到控制臺
pprof cpu_profiler_test cpu_test.profile --text
6.1 文本
Total: 30 samples
6 20.0% 20.0% 16 53.3% psiginfo
5 16.7% 36.7% 5 16.7% __nss_database_lookup
4 13.3% 50.0% 4 13.3% _IO_default_xsputn
4 13.3% 63.3% 28 93.3% __snprintf
3 10.0% 73.3% 3 10.0% _IO_enable_locks
3 10.0% 83.3% 24 80.0% vscanf
2 6.7% 90.0% 2 6.7% _IO_str_pbackfail
1 3.3% 93.3% 1 3.3% cuserid
1 3.3% 96.7% 9 30.0% test_main_thread
1 3.3% 100.0% 21 70.0% test_other_thread
0 0.0% 100.0% 21 70.0% RunFunctionInThread
0 0.0% 100.0% 9 30.0% __libc_start_main
0 0.0% 100.0% 9 30.0% _start
0 0.0% 100.0% 21 70.0% clone
0 0.0% 100.0% 9 30.0% main
0 0.0% 100.0% 21 70.0% start_thread
上面文本中輸出的內(nèi)容是對程序中每一個函數(shù)的CPU使用時間分析憎账,數(shù)據(jù)有兩大列:
- 左:不包含內(nèi)部其他函數(shù)調(diào)用所消耗的CPU時間(內(nèi)聯(lián)函數(shù)除外)如果函數(shù)內(nèi)部沒有任何調(diào)用,那么就和右列相等
- 右:整個函數(shù)消耗的CPU時間卡辰,包括函數(shù)內(nèi)部其他函數(shù)調(diào)用所消耗的CPU時間
每行按照數(shù)據(jù)順序:
- 分析樣本數(shù)量(不包含其他函數(shù)調(diào)用)
- 分析樣本百分比(不包含其他函數(shù)調(diào)用)
- 目前為止的分析樣本百分比(不包含其他函數(shù)調(diào)用)
- 分析樣本數(shù)量(包含其他函數(shù)調(diào)用)
- 分析樣本百分比(包含其他函數(shù)調(diào)用)
- 函數(shù)名
6.2 圖形
每個節(jié)點代表一個函數(shù)胞皱,節(jié)點數(shù)據(jù)格式:
- Class Name
- Method Name
- local (percentage) ,不包含內(nèi)部其他函數(shù)調(diào)用所消耗的CPU時間(內(nèi)聯(lián)函數(shù)除外)
- of cumulative (percentage) 九妈,整個函數(shù)消耗的CPU時間反砌,包括函數(shù)內(nèi)部其他函數(shù)調(diào)用所消耗的CPU時間,如果與local相同萌朱,則不打印
- 有向邊由調(diào)用者指向被調(diào)用者宴树,有向邊上的時間表示被調(diào)用者所消耗的CPU時間
meta 信息(圖左上角):
- Total samples,總采樣數(shù)
- Focusing on晶疼,
--focus
option 所包含的采樣數(shù) - Dropped nodes酒贬,忽略的節(jié)點
- Dropped edges,忽略的邊
focus 某些函數(shù):
pprof --gv --focus=vsnprintf cpu_profiler_test cpu_test.profile
ignore 某些函數(shù):
pprof --gv --ignore=snprintf cpu_profiler_test cpu_test.profile
更多操作可參考:https://gperftools.github.io/gperftools/cpuprofile.html
6.3 Kcachegrind
安裝 Kcachegrind
sudo apt-get install kcachegrind
生成 .callgrind
文件
pprof --callgrind cpu_profiler_test cpu_test.profile > cpu_test.callgrind
分析命令
kcachegrind cpu_test.callgrind
分析結(jié)果如下圖所示翠霍,相對來說功能比較強:7 控制監(jiān)控開關
如果是server上的程序锭吨,啟動后一般不會主動退出,即使退出寒匙,也一般不會正常退出零如,而 gperftools
必須在程序正常退出的情況下才能夠正常收集或者收集完整數(shù)據(jù)。
7.1 請求服務
#include <gperftools/profiler.h>
void on_request(Request* req) {
static bool is_profile_started = false;
if (req->type == START_PROFILE && !is_profile_started) {
ProfilerStart("xxx.profile");
is_profile_started = true;
} else if (req->type == STOP_PROFILE && is_profile_started) {
ProfilerStop();
is_profile_started = false;
} else {
// normal request processing here
}
}
7.2 信號
static void gprof_callback(int signum) {
if (signum == SIGUSR1) {
printf("Catch the signal ProfilerStart\n");
ProfilerStart("bs.prof");
}
else if (signum == SIGUSR2) {
printf("Catch the signal ProfilerStop\n");
ProfilerStop();
}
}
static void setup_signal() {
struct sigaction profstat;
profstat.sa_handler = gprof_callback;
profstat.sa_flags = 0;
sigemptyset(&profstat.sa_mask);
sigaddset(&profstat.sa_mask, SIGUSR1);
sigaddset(&profstat.sa_mask, SIGUSR2);
if (sigaction(SIGUSR1, &profstat,NULL) < 0)
fprintf(stderr, "SIGUSR1 Fail !");
if (sigaction(SIGUSR2, &profstat,NULL) < 0)
fprintf(stderr, "SIGUSR2 Fail !");
}
8 原理
如果只關心如何使用蒋情,則到這里就可以編寫自己的工程了埠况,下面對 CPU profiler 的源碼進行簡單的剖析。
站在巨人的肩膀上:http://www.tealcode.com/gperftool_source_analysis/
入口:
extern “C” PERFTOOLS_DLL_DECL int ProfilerStart(const char* fname) {
return CpuProfiler::instance_.Start(fname, NULL);
}
bool CpuProfiler::Start(const char* fname, const ProfilerOptions* options) {
collector_.Start(fname, collector_options);
// Setup handler for SIGPROF interrupts
EnableHandler();
return true;
}
CPU profiler 啟動的時候棵癣,核心功能就是啟動數(shù)據(jù)收集器(collector_)辕翰,這個數(shù)據(jù)收集器的 Start()
函數(shù)的功能就是初始化數(shù)據(jù)收集需要的數(shù)據(jù)結(jié)構(gòu),并創(chuàng)建數(shù)據(jù)收集文件:
bool ProfileData::Start(const char* fname, const ProfileData::Options& options) {
// Open output file and initialize various data structures
int fd =open(fname, O_CREAT | O_WRONLY | O_TRUNC, 0666);
start_time_ = time(NULL);
fname_ = strdup(fname);
// Reset counters
num_evicted_ = 0;
count_ = 0;
evictions_ = 0;
total_bytes_ = 0;
hash_ = new Bucket[kBuckets];
evict_ = new Slot[kBufferLength];
memset(hash_, 0, sizeof(hash_[0]) * kBuckets);
// Record special entries
evict_[num_evicted_++] = 0; // count for header
evict_[num_evicted_++] = 3; // depth for header
evict_[num_evicted_++] = 0; // Version number
CHECK_NE(0, options.frequency());
int period =1000000/ options.frequency();
evict_[num_evicted_++] = period; // Period (microseconds)
evict_[num_evicted_++] = 0; // Padding
out_ = fd;
return true;
}
然后就是開啟 CPU profiler 的一個處理函數(shù)狈谊,這個函數(shù)就是把 prof_handler()
注冊到了某個地方:
void CpuProfiler::EnableHandler() {
prof_handler_token_ = ProfileHandlerRegisterCallback(prof_handler, this);
}
ProfileHandlerToken* ProfileHandlerRegisterCallback(
ProfileHandlerCallback callback, void* callback_arg) {
return ProfileHandler::Instance()->RegisterCallback(callback, callback_arg);
}
功能都在 ProfileHandler
里面喜命,其為一個單例類沟沙,構(gòu)造函數(shù)如下:
ProfileHandler::ProfileHandler() {
timer_type_ = (getenv(“CPUPROFILE_REALTIME”) ? ITIMER_REAL : ITIMER_PROF);
signal_number_ = (timer_type_ == ITIMER_PROF ? SIGPROF : SIGALRM);
// Get frequency of interrupts (if specified)
char junk;
constchar* fr =getenv(“CPUPROFILE_FREQUENCY”);
if (fr != NULL && (sscanf(fr, “%u%c”, &frequency_, &junk) == 1) && (frequency_ > 0)) {
// Limit to kMaxFrequency
frequency_ = (frequency_ > kMaxFrequency) ? kMaxFrequency : frequency_;
} else {
frequency_ = kDefaultFrequency;
}
// Install the signal handler.
structsigaction sa;
sa.sa_sigaction = SignalHandler;
sa.sa_flags = SA_RESTART | SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(signal_number_, &sa, NULL);
}
構(gòu)造函數(shù)中,根據(jù)環(huán)境變量 CPUPROFILE_REALTIME
的配置壁榕,來決定讓 SIGPROF
還是 SIGALRM
信號來觸發(fā) SignalHandler
信號處理函數(shù)矛紫,并根據(jù)環(huán)境變量 CPUPROFILE_FREQUENCY
的配置來設置自己的一個頻率變量 frequency_
,如果沒有設置牌里,就使用默認值颊咬,這個默認值是100,而最大值是4000牡辽。
然后 ProfileHandler
的 RegisterCallback()
函數(shù)的實現(xiàn)如下:
ProfileHandlerToken* ProfileHandler::RegisterCallback(ProfileHandlerCallback callback, void* callback_arg) {
ProfileHandlerToken* token = new ProfileHandlerToken(callback, callback_arg);
SpinLockHolder cl(&control_lock_);
DisableHandler();
{
SpinLockHolder sl(&signal_lock_);
callbacks_.push_back(token);
}
// Start the timer if timer is shared and this is a first callback.
if ((callback_count_ == 0) && (timer_sharing_ == TIMERS_SHARED)) {
StartTimer();
}
++callback_count_;
EnableHandler();
return token;
}
這個函數(shù)就如其函數(shù)名字喳篇,把指定的回調(diào)函數(shù)添加到 callbacks
_里面去,然后在加入第一個 callback
的時候調(diào)用 StartTimer()
函數(shù)來啟動定時器态辛,然后調(diào)用 EnableHander()
函數(shù)來開啟回調(diào)麸澜。StartTimer()
的實現(xiàn)如下:
void ProfileHandler::StartTimer() {
struct itimerval timer;
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_usec = 1000000 / frequency_;
timer.it_value = timer.it_interval;
setitimer(timer_type_, &timer, 0);
}
EnableHandler()
的實現(xiàn)如下:
void ProfileHandler::EnableHandler() {
struct sigaction sa;
sa.sa_sigaction = SignalHandler;
sa.sa_flags = SA_RESTART | SA_SIGINFO;
sigemptyset(&sa.sa_mask);
const int signal_number = (timer_type_ == ITIMER_PROF ? SIGPROF : SIGALRM);
RAW_CHECK(sigaction(signal_number, &sa, NULL) == 0, “sigprof (enable)”);
}
到這里,這個工具的基本工作原理已經(jīng)可以猜出個大概了奏黑。它用 setitimer()
啟動一個系統(tǒng)定時器炊邦,這個定時器會每秒鐘執(zhí)行觸發(fā) frequency
次 SIGPROF
或者 SIGALRM
信號,從而去觸發(fā)上面注冊的信號處理函數(shù)熟史。那么猜想馁害,信號處理函數(shù)里面應該會用 backtrace 去檢查一下目標程序執(zhí)行到什么位置了。信號處理函數(shù)如下:
void CpuProfiler::prof_handler(int sig, siginfo_t*, void* signal_ucontext, void* cpu_profiler) {
CpuProfiler* instance = static_cast<CpuProfiler*>(cpu_profiler);
if (instance->filter_==NULL||(*instance->filter_)(instance->filter_arg_)) {
void* stack[ProfileData::kMaxStackDepth];
// Under frame-pointer-based unwinding at least on x86, the
// top-most active routine doesn’t show up as a normal frame, but
// as the “pc” value in the signal handler context.
stack[0] = GetPC(*reinterpret_cast<ucontext_t*>(signal_ucontext));
// We skip the top three stack trace entries (this function,
// SignalHandler::SignalHandler and one signal handler frame)
// since they are artifacts of profiling and should not be
// measured. Other profiling related frames may be removed by
// “pprof” at analysis time. Instead of skipping the top frames,
// we could skip nothing, but that would increase the profile size
// unnecessarily.
int depth = GetStackTraceWithContext(stack +1, arraysize(stack) -1, 3, signal_ucontext);
void**used_stack;
if (depth >0&& stack[1] == stack[0]) {
// in case of non-frame-pointer-based unwinding we will get
// duplicate of PC in stack[1], which we don’t want
used_stack = stack + 1;
} else {
used_stack = stack;
depth++; // To account for pc value in stack[0];
}
instance->collector_.Add(depth, used_stack);
}
}
果然是獲取backtrace以故,然后記錄到collector_里面去蜗细。
總結(jié):
- 這個工具是用系統(tǒng)定時器定時產(chǎn)生信號的方式,在信號處理函數(shù)里面獲取當前的調(diào)用堆棧來確定當前落在哪個函數(shù)里面的怒详。獲取頻率默認是每10ms采樣一次炉媒,參數(shù)是可調(diào)的,但是最大頻率是4000昆烁,也就是支持的最小采樣間隔是250微秒吊骤;
- 這個工具獲取到的性能數(shù)據(jù)是基于統(tǒng)計數(shù)據(jù)的,也就是他并不真正跟蹤函數(shù)的每一次調(diào)用過程静尼,而是均勻地采樣并記錄采樣點所落在的函數(shù)調(diào)用位置白粉,用這些統(tǒng)計數(shù)據(jù)來計算每個函數(shù)的執(zhí)行時間占比。這個數(shù)據(jù)并不是準確的數(shù)據(jù)鼠渺,但是只要運行時間相對比較長鸭巴,統(tǒng)計數(shù)據(jù)還是能比較準確地說明問題的。而這也是為什么說這個工具是比較好的服務器程序性能分析工具拦盹,而對一些客戶端程序鹃祖,比如游戲客戶端并不是非常合適。因為游戲客戶端上普舆,相比長時間的統(tǒng)計數(shù)據(jù)恬口,它們通常更加關心的是某些幀內(nèi)的具體負載情況校读。
- 這個工具不工作的時候,就會把系統(tǒng)定時器取消掉祖能,不會定時產(chǎn)生中斷信號歉秫,不會觸發(fā)中斷處理程序,所以對運行程序的影響真的是很小养铸,運行效率上可以說完全沒有影響雁芙。而對產(chǎn)品的影響只是多占用一些鏈接 profiler 庫的內(nèi)存而已。
參考鏈接:
https://gperftools.github.io/gperftools/cpuprofile.html
http://airekans.github.io/cpp/2014/07/04/gperftools-profile
https://blog.csdn.net/aganlengzi/article/details/62893533
https://blog.csdn.net/10km/article/details/83820080
https://www.zhihu.com/question/277160878
http://www.tealcode.com/gperftool_source_analysis/