perf 是 linux 系統中相當重要的一個開發工具,由於是由核心支援對程式設計師能觀測到更多核心層級的資訊。透過幾個常見的 perf 指令例如:perf stat、perf record就能進行大略的分析,並且透過 annotate 從組合語言的角度觀察對指令對 CPU 的影響。然而在實務中,我們會期望對程式更特定的區段進行觀察,我們來看看下面這個例子。
int main() {
boost::random_device dev;
boost::mt19937 rng(dev);
boost::random::uniform_int_distribution<> random(1, 10);
std::vector<int> vector(256);
for (auto &i : vector)
i = random(rng);
#ifdef USE_SORT
std::sort(vector.begin(), vector.end());
#endif
int sum = 0;
for (auto &i : vector) {
if (i > 5)
sum += i;
}
return sum;
}
這段程式碼所測試的是 branch misses,由於 branch 對 CPU 效能是有影響的,現代的 CPU 會透過不同的 branch predictor 來猜測下一步到底該怎走。試想像你是生產線上的工人,但不幸的是這間工廠把鎖螺絲還有釘釘子的工作全部放在同一條產線。你最理想的狀況應該是連續數個工作都能用到同一種工具,減少切換工具的時間。
在上面這個片段中,程式產生了 256 個 1 至 10 的隨機數,如果USE_SORT被定義的話,這些隨機數就會被排序。最後函式會回傳所有數高於 5 的總和(對CPU來說,5 以上以下的數值就像是螺絲與釘子一樣)。如果說,你直接使用 perf 對這段程式進行-e branch-misses測量,最後得出的結果會發現兩者幾乎無異,即便是你指定了執行續還有各種參數,因為最根本的問題是 256 個隨機數實在是太少了。
確實,你可以將 256 個隨機數增到一萬倍,確實會有所幫助,但這並不是一個很有效的辦法。我們也可以測量從加總迴圈開始到結束的時間差,但卻無法得知branch-misses到底佔了多少比重。如果說今天分支觸發的可能是一個相較複雜或成本重的函式,增加執行次數會導致執行時間變得更長、計算時間差無法得知比重。
其實問題的根本是,我們希望能夠對特定程式區段進行測量,我們只要自己開發一個 perf counter 就行。可惜的是 perf 並沒有直接提供 wrapper 接口,我們需要自己寫一個:
int32_t perf_event_open(perf_event_attr *attr, pid_t pid, int32_t cpu,
int32_t group_fd, uint32_t flags) {
return syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags);
}
就像實際操作 perf 通常不會只觀測一種事件,我們能將多種事件組合起來建立為一個 group,只要操作 group 的 leader 就能進行測量:
// attr_<leader|member>.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID;
auto fd_leader = perf_event_open(&attr_leader, getpid(), -1, -1, 0);
ioctl(fd_leader, PERF_EVENT_IOC_ID, &leader_id);
auto fd_member = perf_event_open(&attr_member, getpid(), -1, leader, 0);
ioctl(fd_member, PERF_EVENT_IOC_ID, &member_id);
ioctl(fd_leader, PERF_EVENT_IOC_RESET, PERF_IOC_FLAG_GROUP);
ioctl(fd_leader, PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP);
{
// ...
}
ioctl(fd_leader, PERF_EVENT_IOC_DISABLE, PERF_IOC_FLAG_GROUP);
read(fd_leader, buff, sizeof(buff));
auto rf = (read_format *)buff;
for (uint64_t i = 0; i < rf->nr; ++i) {
auto &result = rf->values[i];
if (result.id == leader_id)
leader_count = result.value;
else if(result.id == member_id)
member_count = result.value;
}
知道接口的操作方式以後我們就能以 C++ 進行包裝,進行更輕鬆的操作。
int main() {
HC::Perf::Counter counter;
std::vector<HC::Perf::Event> events{
{PERF_TYPE_HARDWARE, PERF_COUNT_HW_INSTRUCTIONS},
{PERF_TYPE_HARDWARE, PERF_COUNT_HW_BRANCH_MISSES}};
auto result = counter.set(40000, events.data(), events.size());
if (!result)
exit(EXIT_FAILURE);
boost::random_device dev;
boost::mt19937 rng(dev);
boost::random::uniform_int_distribution<> random(1, 10);
std::vector<int> vector(256);
for (auto &i : vector)
i = random(rng);
#ifdef USE_SORT
std::sort(vector.begin(), vector.end());
#endif
counter.start();
int sum = 0;
for (auto &i : vector) {
if (i > 5)
sum += i;
}
counter.stop();
counter.fetch();
for (const auto &e : events) {
uint64_t count = 0;
counter.get(e, count);
std::cout << e.type << ',' << e.config << ": " << count << std::endl;
}
return sum;
}
如此一來,我們就能更專注在目標量測區域上。