为什么 perf 具有如此高的上下文切换?
why perf has such high context-switches?
我试图理解 linux perf
,发现了一些非常令人困惑的行为:
我写了一个简单的多线程示例,每个内核固定一个线程;每个线程 运行 在本地进行计算并且彼此之间不通信(请参阅下面的 test.cc
)。我在想这个例子应该有非常低的上下文切换,如果不是零的话。但是,使用 linux perf
分析示例显示了数千个上下文切换 - 比我预期的要多得多。我进一步分析了 linux 命令 sleep 20
以进行比较,显示上下文切换要少得多。
这个个人资料结果对我来说没有任何意义。是什么导致如此多的上下文切换?
> sudo perf stat -e sched:sched_switch ./test
Performance counter stats for './test':
6,725 sched:sched_switch
20.835 seconds time elapsed
> sudo perf stat -e sched:sched_switch sleep 20
Performance counter stats for 'sleep 20':
1 sched:sched_switch
20.001 seconds time elapsed
要重现结果,请运行以下代码:
perf stat -e context-switches sleep 20
perf stat -e context-switches ./test
要编译源代码,请输入以下代码:
g++ -std=c++11 -pthread -o test test.cc
// test.cc
#include <iostream>
#include <thread>
#include <vector>
int main(int argc, const char** argv) {
unsigned num_cpus = std::thread::hardware_concurrency();
std::cout << "Launching " << num_cpus << " threads\n";
std::vector<std::thread> threads(num_cpus);
for (unsigned i = 0; i < num_cpus; ++i) {
threads[i] = std::thread([i] {
int j = 0;
while (j++ < 100) {
int tmp = 0;
while (tmp++ < 110000000) { }
}
});
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(i, &cpuset);
int rc = pthread_setaffinity_np(threads[i].native_handle(),
sizeof(cpu_set_t), &cpuset);
if (rc != 0) {
std::cerr << "Error calling pthread_setaffinity_np: " << rc << "\n";
}
}
for (auto& t : threads) {
t.join();
}
return 0;
}
我们无法准确告诉您具体安排了什么 - 但您可以使用 perf
.
自行查找
perf record -e sched:sched_switch ./test
请注意,这需要已安装的 debugfs 和 root 权限。现在 perf report
将为您提供调度程序切换到的内容的概述(或查看 perf script
以获取完整列表)。现在你的代码中没有明显的事情会导致上下文切换(例如睡眠,等待 I/O),所以很可能是在这些核心上安排了另一个任务。
sleep
几乎没有上下文切换的原因很简单。它几乎立即进入睡眠状态——这是一个上下文切换。当任务未激活时,它不能被另一个任务取代。
您可以使用 sudo perf sched record -- ./test
来确定哪些进程被调度到 运行 以代替应用程序的线程之一。当我在我的系统上执行这个命令时,我得到:
sudo perf sched record -- ./test
Launching 4 threads
[ perf record: Woken up 10 times to write data ]
[ perf record: Captured and wrote 23.886 MB perf.data (212100 samples) ]
请注意,我有四个内核,可执行文件的名称是test
。 perf sched
已捕获所有 sched:sched_switch
事件并将数据转储到默认情况下名为 perf.data
的文件中。该文件的大小约为 23 MB,包含大约 212100 个事件。分析的持续时间将从 perf
开始到 test
终止。
您可以使用 sudo perf sched map
以如下所示的漂亮格式打印所有记录的事件:
*. 448826.757400 secs . => swapper:0
*A0 448826.757461 secs A0 => perf:15875
*. A0 448826.757477 secs
*. . A0 448826.757548 secs
. . *B0 448826.757601 secs B0 => migration/3:22
. . *. 448826.757608 secs
*A0 . . 448826.757625 secs
A0 *C0 . 448826.757775 secs C0 => rcu_sched:7
A0 *. . 448826.757777 secs
*D0 . . 448826.757803 secs D0 => ksoftirqd/0:3
*A0 . . 448826.757807 secs
A0 *E0 . . 448826.757862 secs E0 => kworker/1:3:13786
A0 *F0 . . 448826.757870 secs F0 => kworker/1:0:5886
A0 *G0 . . 448826.757874 secs G0 => hud-service:1609
A0 *. . . 448826.758614 secs
A0 *H0 . . 448826.758714 secs H0 => kworker/u8:2:15585
A0 *. . . 448826.758721 secs
A0 . *I0 . 448826.758740 secs I0 => gnome-terminal-:8878
A0 . I0 *J0 448826.758744 secs J0 => test:15876
A0 . I0 *B0 448826.758749 secs
两个字母的名字A0
、B0
、C0
、E0
等等,都是perf
给每个人起的简称系统上的线程 运行ning。前四列显示哪个线程在四个核心中的每一个上 运行ning。例如,在倒数第二行中,您可以看到在 for
循环中创建的第一个线程。分配给该线程的名称是 J0
。该线程 运行ning 在第四个核心上。星号表示它刚刚从某个其他线程进行了上下文切换。没有星号,表示同一个线程在同一个核心上继续 运行 另一个时间片。一个点代表一个空闲的内核。要确定所有四个线程的名称,运行 以下命令:
sudo perf sched map | grep 'test'
在我的系统上,打印出:
A0 . I0 *J0 448826.758744 secs J0 => test:15876
J0 A0 *K0 . 448826.758868 secs K0 => test:15878
J0 *L0 K0 . 448826.758889 secs L0 => test:15877
J0 L0 K0 *M0 448826.758894 secs M0 => test:15879
现在您知道了分配给您的线程(以及所有其他线程)的两个字母的名称。您可以确定哪些其他线程导致您的线程进行上下文切换。例如,如果您看到这个:
*G1 L0 K0 M0 448826.822555 secs G1 => firefox:2384
然后您就会知道您的三个应用程序线程正在 运行ning,但其中一个内核正用于 运行 Firefox。所以第四个线程需要等到调度器决定什么时候再调度。
如果您想要至少有一个线程占用的所有调度程序插槽,那么您可以使用以下命令:
sudo perf sched map > mydata
grep -E 'J0|K0|L0|M0' mydata > mydata2
wc -l mydata
wc -l mydata2
最后两个命令告诉您应用的至少一个线程有多少行(时间片)运行正在运行。您可以将其与时间片的总数进行比较。由于有四个核心,调度程序槽的总数为 4 *(时间片数)。然后您可以进行各种手动计算并弄清楚到底发生了什么。
我试图理解 linux perf
,发现了一些非常令人困惑的行为:
我写了一个简单的多线程示例,每个内核固定一个线程;每个线程 运行 在本地进行计算并且彼此之间不通信(请参阅下面的 test.cc
)。我在想这个例子应该有非常低的上下文切换,如果不是零的话。但是,使用 linux perf
分析示例显示了数千个上下文切换 - 比我预期的要多得多。我进一步分析了 linux 命令 sleep 20
以进行比较,显示上下文切换要少得多。
这个个人资料结果对我来说没有任何意义。是什么导致如此多的上下文切换?
> sudo perf stat -e sched:sched_switch ./test
Performance counter stats for './test':
6,725 sched:sched_switch
20.835 seconds time elapsed
> sudo perf stat -e sched:sched_switch sleep 20
Performance counter stats for 'sleep 20':
1 sched:sched_switch
20.001 seconds time elapsed
要重现结果,请运行以下代码:
perf stat -e context-switches sleep 20
perf stat -e context-switches ./test
要编译源代码,请输入以下代码:
g++ -std=c++11 -pthread -o test test.cc
// test.cc
#include <iostream>
#include <thread>
#include <vector>
int main(int argc, const char** argv) {
unsigned num_cpus = std::thread::hardware_concurrency();
std::cout << "Launching " << num_cpus << " threads\n";
std::vector<std::thread> threads(num_cpus);
for (unsigned i = 0; i < num_cpus; ++i) {
threads[i] = std::thread([i] {
int j = 0;
while (j++ < 100) {
int tmp = 0;
while (tmp++ < 110000000) { }
}
});
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(i, &cpuset);
int rc = pthread_setaffinity_np(threads[i].native_handle(),
sizeof(cpu_set_t), &cpuset);
if (rc != 0) {
std::cerr << "Error calling pthread_setaffinity_np: " << rc << "\n";
}
}
for (auto& t : threads) {
t.join();
}
return 0;
}
我们无法准确告诉您具体安排了什么 - 但您可以使用 perf
.
perf record -e sched:sched_switch ./test
请注意,这需要已安装的 debugfs 和 root 权限。现在 perf report
将为您提供调度程序切换到的内容的概述(或查看 perf script
以获取完整列表)。现在你的代码中没有明显的事情会导致上下文切换(例如睡眠,等待 I/O),所以很可能是在这些核心上安排了另一个任务。
sleep
几乎没有上下文切换的原因很简单。它几乎立即进入睡眠状态——这是一个上下文切换。当任务未激活时,它不能被另一个任务取代。
您可以使用 sudo perf sched record -- ./test
来确定哪些进程被调度到 运行 以代替应用程序的线程之一。当我在我的系统上执行这个命令时,我得到:
sudo perf sched record -- ./test
Launching 4 threads
[ perf record: Woken up 10 times to write data ]
[ perf record: Captured and wrote 23.886 MB perf.data (212100 samples) ]
请注意,我有四个内核,可执行文件的名称是test
。 perf sched
已捕获所有 sched:sched_switch
事件并将数据转储到默认情况下名为 perf.data
的文件中。该文件的大小约为 23 MB,包含大约 212100 个事件。分析的持续时间将从 perf
开始到 test
终止。
您可以使用 sudo perf sched map
以如下所示的漂亮格式打印所有记录的事件:
*. 448826.757400 secs . => swapper:0
*A0 448826.757461 secs A0 => perf:15875
*. A0 448826.757477 secs
*. . A0 448826.757548 secs
. . *B0 448826.757601 secs B0 => migration/3:22
. . *. 448826.757608 secs
*A0 . . 448826.757625 secs
A0 *C0 . 448826.757775 secs C0 => rcu_sched:7
A0 *. . 448826.757777 secs
*D0 . . 448826.757803 secs D0 => ksoftirqd/0:3
*A0 . . 448826.757807 secs
A0 *E0 . . 448826.757862 secs E0 => kworker/1:3:13786
A0 *F0 . . 448826.757870 secs F0 => kworker/1:0:5886
A0 *G0 . . 448826.757874 secs G0 => hud-service:1609
A0 *. . . 448826.758614 secs
A0 *H0 . . 448826.758714 secs H0 => kworker/u8:2:15585
A0 *. . . 448826.758721 secs
A0 . *I0 . 448826.758740 secs I0 => gnome-terminal-:8878
A0 . I0 *J0 448826.758744 secs J0 => test:15876
A0 . I0 *B0 448826.758749 secs
两个字母的名字A0
、B0
、C0
、E0
等等,都是perf
给每个人起的简称系统上的线程 运行ning。前四列显示哪个线程在四个核心中的每一个上 运行ning。例如,在倒数第二行中,您可以看到在 for
循环中创建的第一个线程。分配给该线程的名称是 J0
。该线程 运行ning 在第四个核心上。星号表示它刚刚从某个其他线程进行了上下文切换。没有星号,表示同一个线程在同一个核心上继续 运行 另一个时间片。一个点代表一个空闲的内核。要确定所有四个线程的名称,运行 以下命令:
sudo perf sched map | grep 'test'
在我的系统上,打印出:
A0 . I0 *J0 448826.758744 secs J0 => test:15876
J0 A0 *K0 . 448826.758868 secs K0 => test:15878
J0 *L0 K0 . 448826.758889 secs L0 => test:15877
J0 L0 K0 *M0 448826.758894 secs M0 => test:15879
现在您知道了分配给您的线程(以及所有其他线程)的两个字母的名称。您可以确定哪些其他线程导致您的线程进行上下文切换。例如,如果您看到这个:
*G1 L0 K0 M0 448826.822555 secs G1 => firefox:2384
然后您就会知道您的三个应用程序线程正在 运行ning,但其中一个内核正用于 运行 Firefox。所以第四个线程需要等到调度器决定什么时候再调度。
如果您想要至少有一个线程占用的所有调度程序插槽,那么您可以使用以下命令:
sudo perf sched map > mydata
grep -E 'J0|K0|L0|M0' mydata > mydata2
wc -l mydata
wc -l mydata2
最后两个命令告诉您应用的至少一个线程有多少行(时间片)运行正在运行。您可以将其与时间片的总数进行比较。由于有四个核心,调度程序槽的总数为 4 *(时间片数)。然后您可以进行各种手动计算并弄清楚到底发生了什么。