为什么性能测试中有如此高的上下文切换?

13

我试图理解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;
}

1
如果您打开优化,会发生什么? - YSC
2
尝试运行 top 命令,查看是否有其他并行运行的程序占用了 CPU,例如网络浏览器。并行任务可能会导致所有这些上下文切换。 - Michael Veksler
4
您正在启动与CPU数量相同的线程,因此操作系统希望运行的任何其他内容都需要暂停其中一个线程。这包括图形桌面上闪烁的光标、时钟或许多Linux发行版默认运行的任何垃圾后台进程。或者像Michael提到的那样,使用打开任何JavaScript循环的选项卡的Web浏览器,或其他任何东西。(但不包括硬件中断处理程序。也许是在内核线程中运行的底半部分IRQ Linux处理程序,但不是直接从int运行的“顶半部分”处理程序) - Peter Cordes
1
很难消除其他机器上正在进行的竞争(如前所述),但您可以通过运行“好的-n-20...”来将进程优先级更改为最高。另外:这是在虚拟机中运行还是本地运行? 这也会产生很大的影响。 - aaron
好问题 - 但请不要包含文本截图 - 总是将文本本身放在其中。 - Zulan
2个回答

9
您可以使用命令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) ]

请注意,我的计算机有四个核心,可执行文件的名称为testperf 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 给系统上运行的每个线程分配的短名称。前四列显示了每个核心上运行的线程。例如,在倒数第二行中,您可以看到在循环中创建的第一个线程。给该线程分配的名称为 J0。该线程正在第四个核心上运行。星号表示它刚刚被从其他线程切换过来。没有星号,则表示同一线程继续在同一核心上运行另一个时间片。点表示空闲核心。要确定所有四个线程的名称,请运行以下命令:

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

如果你了解的话,你会知道你的应用程序有三个线程在运行,但其中一个核心正在用来运行Firefox。所以第四个线程需要等待调度程序决定何时再次安排。
如果您想要占用至少一个线程的所有调度程序槽位,则可以使用以下命令:
sudo perf sched map > mydata
grep -E 'J0|K0|L0|M0' mydata > mydata2
wc -l mydata
wc -l mydata2

最后两个命令将告诉您有多少行(时间片)至少有一个线程在运行。您可以将其与总时间片数进行比较。由于有四个核心,调度程序插槽的总数是4 *(时间片数)。然后,您可以进行各种手动计算并找出发生了什么。


7

我们无法告诉您正在安排什么 - 但您可以使用perf自行找出。

perf record -e sched:sched_switch ./test

请注意,这需要挂载debugfs和root权限。现在,perf report将为您提供调度程序正在切换到的概述(或者请参见perf script获取完整列表)。现在,在您的代码中没有明显的东西会导致上下文切换(例如睡眠、等待I/O),因此很可能是另一个任务在这些核心上被调度。 sleep 几乎没有上下文切换的原因很简单。它几乎立即进入睡眠状态-这是一次上下文切换。在任务不活动时,它不能被另一个任务替代。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接