跨上下文切换使用 rdtsc + rdtscp

Using rdtsc + rdtscp across context switch

我正在尝试编写一个程序来测量上下文切换。我已经完成了关于 rdtsc + rdtscp 指令的Intel's manual

现在,我想跨上下文切换使用这些时间戳指令。我的大致骨架如下:

// init two pipes P1, P2
fork();

set_affinity();              // same core

// parent's code:
    cpuid + rdtsc            // start timer
    write(timer to P1);

    read(timer from P2);     // blocks parent if timer value not written
    rdtscp + cpuid           // stop timer, get difference

// child's code:
    read(timer from P1);     // blocks child if timer value not written
    rdtscp + cpuid           // stop timer, get difference

    cpuid + rdtsc            // start timer
    write(timer to P2);

我发现这段代码存在一些问题。假设计时器操作正确,

如果 OS 选择上下文切换到某个完全不同的进程(不是子进程或父进程),它将不起作用。

此代码还将包括 read() 和 write() 系统调用所用的时间。

忽略这些问题,是否有效使用 rdtsc + rdtscp 指令?

I know writing a kernel module and disabling preemption/interrupts is a better way

我以前做过,它似乎是测量上下文切换时间的有效方法。每当对这种细粒度的事情进行计时时,调度的不可预测性总是会发挥作用;通常,您通过测量数千次并寻找诸如最小时间间隔、媒体时间间隔或平均时间间隔之类的数字来处理这个问题。您可以通过 运行 使两个进程具有实时 SCHED_FIFO 优先级来减少调度问题。如果您想知道实际的 切换 时间(在单个 cpu 核心上),您需要将两个进程绑定到具有亲和力设置的单个 cpu。如果您只想知道一个进程能够响应另一个进程的输出的延迟,让它们 运行 在不同的 cpu 上就可以了。

另一个要记住的问题是,自愿和非自愿的上下文切换,以及从 user-space 和 kernel-space 开始的切换有不同的成本。你的很可能是自愿的。非自愿测量更难,需要从繁忙的循环或类似的情况中查看共享内存。

我使用了类似的计时代码,除了我有 1000000 次父循环,并在父子循环中为整个循环计时。附上代码。然后我修改它以计时各个上下文切换,就像在您的伪代码中一样,将 1000000 个单独时间相加并与我的原始代码非常一致。因此,考虑到已经提到的注意事项,任何一种方式似乎都有效。

我觉得有趣的是,当使用 sched_setaffinity() 将父项和子项设置为 运行 时,上下文切换时间增加了一倍多 cpus。为什么这会以这种方式影响时间?在同一 cpu 上进程 运行ning 之间的管道是否更快?

rdtscp.h:

static inline unsigned long rdtscp_start(void) {
  unsigned long var;
  unsigned int hi, lo;

  __asm volatile ("cpuid\n\t"
          "rdtsc\n\t" : "=a" (lo), "=d" (hi)
          :: "%rbx", "%rcx");

  var = ((unsigned long)hi << 32) | lo;
  return (var);
}

static inline unsigned long rdtscp_end(void) {
  unsigned long var;
  unsigned int hi, lo;

  __asm volatile ("rdtscp\n\t"
          "mov %%edx, %1\n\t"
          "mov %%eax, %0\n\t"
          "cpuid\n\t"  : "=r" (lo), "=r" (hi)
          :: "%rax", "%rbx", "%rcx", "%rdx");

  var = ((unsigned long)hi << 32) | lo;
  return (var);
  }

/*see https://www.intel.com/content/www/us/en/embedded/training/ia-32-ia-64-benchmark-code-execution-paper.html
 */

cntxtSwtchr.c:

#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "rdtscp.h"

int main() {
  int pipe1[2], pipe2[2];
  pipe(pipe1) || pipe(pipe2);
  cpu_set_t set;
  CPU_ZERO(&set);

  clock_t tick, tock;

  int fork_rtn;
  if ((fork_rtn = fork()) < 0)
    exit(1);

  if (fork_rtn == 0) {  // Child
    close(pipe1[1]);
    close(pipe2[0]);

    CPU_SET(1, &set);
    sched_setaffinity(0, sizeof(set), &set);

    tick = clock();
    unsigned long tsc_start = rdtscp_start();
    int i;
    while (read(pipe1[0], &i, 4)) 
      write(pipe2[1], &i, 4);
    printf("child tsc_ticks: %lu\n", rdtscp_end() - tsc_start);
    tock = clock();
    clock_t ticks = tock - tick;
    double dt = (double)ticks / CLOCKS_PER_SEC;
    printf("Elapsed child cpu time: %gs.\n", dt); 

    close(pipe1[0]);
    close(pipe2[1]);
    exit(0);

  } else {              // Parent
    close(pipe1[0]);
    close(pipe2[1]);

    CPU_SET(1, &set);
    sched_setaffinity(0, sizeof(set), &set);

    int idx, lim = 1000000;
    int i_rtnd;
    tick = clock();
    unsigned long tsc_start = rdtscp_start();
    for (idx = 0; idx < lim; ++idx) {
      write(pipe1[1], &idx, 4);
      read(pipe2[0], &i_rtnd, 4);
      if (i_rtnd != idx) 
    break;
    }
    printf("parent tsc_ticks: %lu\n", rdtscp_end() - tsc_start);
    tock = clock();
    clock_t ticks = tock - tick;
    double dt = (double)ticks / CLOCKS_PER_SEC;
    printf("Elapsed parent cpu time: %gs, %gs/switch.\n", dt, dt / lim); 
    if (idx == lim)
      printf("Parent reached end of processing loop.\n");
    else
      printf("Parent failed to reach end of processing loop.\n");

    close(pipe1[1]);
    close(pipe2[0]);
    exit(0);
  }

}