为什么perf有这么高的上下文切换?

dao*_*ker 9 c++ linux perf

我试图了解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
Run Code Online (Sandbox Code Playgroud)

要重现结果,请运行以下代码:

perf stat -e context-switches sleep 20
perf stat -e context-switches ./test
Run Code Online (Sandbox Code Playgroud)

要编译源代码,请输入以下代码:

g++ -std=c++11 -pthread -o test test.cc
Run Code Online (Sandbox Code Playgroud)
// 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;
}
Run Code Online (Sandbox Code Playgroud)

Zul*_*lan 6

我们无法确切地告诉您正在安排的内容 - 但您可以发现自己正在使用perf.

perf record -e sched:sched_switch ./test
Run Code Online (Sandbox Code Playgroud)

请注意,这需要已安装的debugfs和root权限.现在,我们perf report将概述调度程序切换到的内容(或查看perf script完整列表).现在,代码中没有明显的事情会导致上下文切换(例如,休眠,等待I/O),因此很可能是在这些内核上安排的另一个任务.

sleep几乎没有上下文切换的原因很简单.它几乎立即进入睡眠状态 - 这是一个上下文切换.当任务未激活时,它不能被另一个任务取代.


Had*_*ais 5

您可以使用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) ]
Run Code Online (Sandbox Code Playgroud)

请注意,我有四个内核,可执行文件的名称是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 
Run Code Online (Sandbox Code Playgroud)

由两个字母组成的名称A0B0C0、等是系统上运行的每个线程的E0简称perf。前四列显示了在四个内核中的每一个上运行的线程。例如,在倒数第二行中,您可以看到在for循环中创建的第一个线程。分配给该线程的名称是J0。该线程正在第四个内核上运行。星号表示它刚刚从某个其他线程上下文切换到。如果没有星号,则表示同一个线程在同一个内核上继续运行了另一个时间片。一个点代表一个空闲的核心。要确定所有四个线程的名称,请运行以下命令:

sudo perf sched map | grep 'test'
Run Code Online (Sandbox Code Playgroud)

在我的系统上,这会打印:

   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
Run Code Online (Sandbox Code Playgroud)

现在您知道分配给您的线程(和所有其他线程)的两个字母的名称。您可以确定哪些其他线程导致您的线程进行上下文切换。例如,如果你看到这个:

  *G1  L0  K0  M0       448826.822555 secs G1 => firefox:2384
Run Code Online (Sandbox Code Playgroud)

那么您就会知道您的三个应用程序线程正在运行,但其中一个内核用于运行 Firefox。所以第四个线程需要等到调度器决定什么时候再调度。

如果您想要至少一个线程占用的所有调度程序插槽,则可以使用以下命令:

sudo perf sched map > mydata
grep -E 'J0|K0|L0|M0' mydata > mydata2
wc -l mydata
wc -l mydata2
Run Code Online (Sandbox Code Playgroud)

最后两个命令告诉您应用程序的至少一个线程正在运行的行数(时间片)。您可以将其与时间片的总数进行比较。由于有四个内核,因此调度程序槽的总数为 4 *(时间片数)。然后,您可以进行各种手动计算并弄清楚到底发生了什么。