我想知道是否有可能属于具有相同 PCID 的同一程序的两个线程在计划在同一物理 CPU 上运行时可以共享 TLB 条目?
我已经研究过 SDM(https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html);第 3115 页(TLB 和 HT)没有提及任何共享机制。但是文档的另一部分指出,在访问 TLB 条目之前,会检查 PCID value,如果相等,则使用 value。但是,PCID 标识符旁边还有一个用于当前线程集的位。
我的问题:PCID value 使用的优先级是否高于 CPU 线程位,或者两者是否有必要 values 匹配?
回答1
根据我的观察,这是不可能的(至少对于 dTLB
而言),即使它会带来性能优势。
我是如何得出这个结论的
按照 Peter 的建议,我编写了一个小程序,它由两个工作线程组成,它们一遍又一遍地访问同一个堆区域。
使用 -O0
编译以防止优化。
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <inttypes.h>
#include <err.h>
#include <sched.h>
#include <sys/mman.h>
#define PAGE_SIZE 4096
int repetitions = 1ll << 20;
uint64_t ptrsize = 1ll<<18;
uint64_t main_cpu, co_cpu ;
void pin_task_to(int pid, int cpu)
{
cpu_set_t cset;
CPU_ZERO(&cset);
CPU_SET(cpu, &cset);
if (sched_setaffinity(pid, sizeof(cpu_set_t), &cset))
err(1, "affinity");
}
void pin_to(int cpu) { pin_task_to(0, cpu); }
void *foo(void *p)
{
pin_to(main_cpu);
int value;
uint8_t *ptr = (uint8_t *)p;
printf("Running on CPU: %d\n", sched_getcpu());
for (size_t j = 0; j < repetitions; j++)
{
for (size_t i = 0; i < ptrsize; i += PAGE_SIZE)
{
value += ptr[i];
}
}
volatile int dummy = value;
pthread_exit(NULL);
}
void *boo(void *p)
{
pin_to(co_cpu);
int value;
uint8_t *ptr = (uint8_t *)p;
printf("Running on CPU: %d\n", sched_getcpu());
for (size_t j = 0; j < repetitions; j++)
{
for (size_t i = 0; i < ptrsize; i+=PAGE_SIZE)
{
value += ptr[i];
}
}
volatile int dummy = value;
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
if (argc < 3){
exit(-1);
}
main_cpu = strtoul(argv[1], NULL, 16);
co_cpu = strtoul(argv[2], NULL, 16);
pthread_t id[2];
void *mptr = malloc(ptrsize);
pthread_create(&id[0], NULL, foo, mptr);
pthread_create(&id[1], NULL, boo, mptr);
pthread_join(id[0], NULL);
pthread_join(id[1], NULL);
}
我决定将内存区域中的所有values(显然value
会溢出)进行汇总,以防止CPU进行微架构优化。
[另一个想法是简单地逐字节取消引用内存区域并在 RAX
中加载 value]
我们遍历内存区域 repetitions
次,以减少一次运行中由于线程和其他进程的启动时间略有不同以及系统上的中断而引起的噪音。
结果
我的机器有四个物理核心和八个逻辑核心。逻辑核心 x 和 x+4 位于同一个物理核心 (lstopo) 上。
在同一个逻辑核心上运行
由于内核使用 PCID 来识别 TLB 条目,因此到另一个线程的上下文切换不应使 TLBs 无效。
> $ perf stat -e dtlb_load_misses.stlb_hit,dtlb_load_misses.miss_causes_a_walk,cycles,task-clock ./main 1 1
Running on CPU: 1
Running on CPU: 1
Performance counter stats for './main 1 1':
12,621,724 dtlb_load_misses.stlb_hit:u # 49.035 M/sec
1,152 dtlb_load_misses.miss_causes_a_walk:u # 4.475 K/sec
834,363,092 cycles:u # 3.241 GHz
257.40 msec task-clock:u # 0.997 CPUs utilized
0.258177969 seconds time elapsed
0.258253000 seconds user
0.000000000 seconds sys
在两个不同的物理内核上运行
没有任何 TLB 共享或干扰。
> $ perf stat -e dtlb_load_misses.stlb_hit,dtlb_load_misses.miss_causes_a_walk,cycles,task-clock ./main 1 2
Running on CPU: 1
Running on CPU: 2
Performance counter stats for './main 1 2':
11,740,758 dtlb_load_misses.stlb_hit:u # 45.962 M/sec
1,647 dtlb_load_misses.miss_causes_a_walk:u # 6.448 K/sec
834,021,644 cycles:u # 3.265 GHz
255.44 msec task-clock:u # 1.991 CPUs utilized
0.128304564 seconds time elapsed
0.255768000 seconds user
0.000000000 seconds sys
在同一个物理内核上运行
如果 TLB 共享是可能的,我希望这里有最低的 sTLB
命中和少量的 dTLB
页面浏览。但相反,在这两种情况下,我们的数字都是最高的。
> $ perf stat -e dtlb_load_misses.stlb_hit,dtlb_load_misses.miss_causes_a_walk,cycles,task-clock ./main 1 5
Running on CPU: 1
Running on CPU: 5
Performance counter stats for './main 1 5':
140,040,429 dtlb_load_misses.stlb_hit:u # 291.368 M/sec
198,827 dtlb_load_misses.miss_causes_a_walk:u # 413.680 K/sec
1,596,298,827 cycles:u # 3.321 GHz
480.63 msec task-clock:u # 1.990 CPUs utilized
0.241509701 seconds time elapsed
0.480996000 seconds user
0.000000000 seconds sys
结论
如您所见,在同一物理内核上运行时,我们的 sTLB
命中和 dTLB
页面访问次数最多。因此,我认为在同一个物理内核上没有相同的 PCID 共享机制。在同一个逻辑核心和两个不同的物理核心上运行该进程会导致 sTLB 的未命中/命中数大致相同。这进一步支持了在同一个逻辑核心上共享但在物理核心上没有共享的论点。
更新
正如彼得所建议的那样,还使用链表方法来防止 THP 和预取。修改后的数据如下所示。
使用 -O0
编译以防止优化
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <inttypes.h>
#include <err.h>
#include <sched.h>
#include <time.h>
#include <sys/mman.h>
#define PAGE_SIZE 4096
const int repetitions = 1ll << 20;
const uint64_t ptrsize = 1ll<< 5;
uint64_t main_cpu, co_cpu ;
void pin_task_to(int pid, int cpu)
{
cpu_set_t cset;
CPU_ZERO(&cset);
CPU_SET(cpu, &cset);
if (sched_setaffinity(pid, sizeof(cpu_set_t), &cset))
err(1, "affinity");
}
void pin_to(int cpu) { pin_task_to(0, cpu); }
void *foo(void *p)
{
pin_to(main_cpu);
uint64_t *value;
uint64_t *ptr = (uint64_t *)p;
printf("Running on CPU: %d\n", sched_getcpu());
for (size_t j = 0; j < repetitions; j++)
{
value = ptr;
for (size_t i = 0; i < ptrsize; i++)
{
value = (uint64_t *)*value;
}
}
volatile uint64_t *dummy = value;
pthread_exit(NULL);
}
void *boo(void *p)
{
pin_to(co_cpu);
uint64_t *value;
uint64_t *ptr = (uint64_t *)p;
printf("Running on CPU: %d\n", sched_getcpu());
for (size_t j = 0; j < repetitions; j++)
{
value = ptr;
for (size_t i = 0; i < ptrsize; i++)
{
value = (uint64_t *)*value;
}
}
volatile uint64_t *dummy = value;
pthread_exit(NULL);
}
int main(int argc, char **argv)
{
if (argc < 3){
exit(-1);
}
srand(time(NULL));
uint64_t *head,*tail,*tmp_ptr;
int r;
head = mmap(NULL,PAGE_SIZE,PROT_READ|PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS,0,0);
tail = head;
for (size_t i = 0; i < ptrsize; i++)
{
r = (rand() & 0xF) +1;
// try to use differents offset to the next page to prevent microarch prefetching
tmp_ptr = mmap(tail-r*PAGE_SIZE, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
*tail = (uint64_t)tmp_ptr;
tail = tmp_ptr;
}
printf("%Lx, %lx\n", head, *head);
main_cpu = strtoul(argv[1], NULL, 16);
co_cpu = strtoul(argv[2], NULL, 16);
pthread_t id[2];
pthread_create(&id[0], NULL, foo, head);
pthread_create(&id[1], NULL, boo, head);
pthread_join(id[0], NULL);
pthread_join(id[1], NULL);
}
相同的逻辑核心
> $ perf stat -e dtlb_load_misses.stlb_hit,dtlb_load_misses.miss_causes_a_walk,cycles,task-clock ./main 1 1
7feac4d90000, 7feac4d5b000
Running on CPU: 1
Running on CPU: 1
Performance counter stats for './main 1 1':
3,696 dtlb_load_misses.stlb_hit:u # 11.679 K/sec
743 dtlb_load_misses.miss_causes_a_walk:u # 2.348 K/sec
762,856,367 cycles:u # 2.410 GHz
316.48 msec task-clock:u # 0.998 CPUs utilized
0.317105072 seconds time elapsed
0.316859000 seconds user
0.000000000 seconds sys
不同的物理内核
> $ perf stat -e dtlb_load_misses.stlb_hit,dtlb_load_misses.miss_causes_a_walk,cycles,task-clock ./main 1 2
7f59bb395000, 7f59bb34d000
Running on CPU: 1
Running on CPU: 2
Performance counter stats for './main 1 2':
15,144 dtlb_load_misses.stlb_hit:u # 49.480 K/sec
756 dtlb_load_misses.miss_causes_a_walk:u # 2.470 K/sec
770,800,780 cycles:u # 2.518 GHz
306.06 msec task-clock:u # 1.982 CPUs utilized
0.154410840 seconds time elapsed
0.306345000 seconds user
0.000000000 seconds sys
相同的物理内核/不同的逻辑内核
> $ perf stat -e dtlb_load_misses.stlb_hit,dtlb_load_misses.miss_causes_a_walk,cycles,task-clock ./main 1 5
7f7d69e8b000, 7f7d69e56000
Running on CPU: 5
Running on CPU: 1
Performance counter stats for './main 1 5':
9,237,992 dtlb_load_misses.stlb_hit:u # 20.554 M/sec
789 dtlb_load_misses.miss_causes_a_walk:u # 1.755 K/sec
1,007,185,858 cycles:u # 2.241 GHz
449.45 msec task-clock:u # 1.989 CPUs utilized
0.225947522 seconds time elapsed
0.449813000 seconds user
0.000000000 seconds sys