首页 > 其他分享 >【转帖】基础软件开发 -- 神秘的MESI和坑爹的LockFree

【转帖】基础软件开发 -- 神秘的MESI和坑爹的LockFree

时间:2024-02-07 15:45:00浏览次数:33  
标签:Core -- TSC LockFree 转帖 内存 Line 权限 CPU

https://zhuanlan.zhihu.com/p/681321783

 

又开新坑,继续掰扯基础软件开发。这里已经更新到第二季了,欲先睹为快的可以到这里:

基础软件开发新坑 -- 神秘的MESI和坑爹的LockFree(一)

基础软件开发新坑 -- 神秘的MESI和坑爹的LockFree(二)

正文开始:

在《HPC(高性能计算第一篇) :一文彻底搞懂并发编程与内存屏障》中,我简述了CPU的架构:

图1

图1是一个双Socket(基本上可以认为就是双路),每个Socket中是一个大CPU,每个大CPU中有两个Core的架构图。

当然,真实的一个大CPU,Core数往往多于2个。这里画不下了,大家凑合着看。

图1中的CPU 0、CPU 1……,准确叫法应该是Core 0、Core 1……。

不过,习惯上称它们为CPU 0/2/3/4也可以。(后文中时常混用CPU、核,标准写法应该是Core、核)

内存屏障那篇文章中,我们简单提了一嘴,CPU之间是需要通信的,还有一个MESI协议。本文就是针对这两项的,MESI协议,和CPU间的通信。

关于MESI协议,好多资料都是再讲它的实现,是在教你如何搞出一个MESI协议,至于它对程序现实意义上的影响,鲜有涉及。

本文就抛开这些文绉绉的理论,和严谨的用词,从实践上了解下MESI协议、核间通信,对程序现实上的影响。

有好多朋友没有深入的了解过MESI,所以开头先简单的普及下基本理论。

MESI协议,其本质意义是:CPU核间通信延迟情况下的产物,目的是为了减少核间通信的次数。

多核时代,内存是共享的。有可能一个Core要读、写的任何内存数据(内存变量),同时有另一个Core也要读、写。

就好像一条公路,不是你家庭院自己的路,可能你每走一步,都会撞到别人。怎么办?

你要每时每刻都用眼去观察对方,没人和你抢道时,你向前走。有人抢了你的道时,为了不撞车,你也只能等。

CPU也是这样,理论上,每一次内存读、写,都要确认没有其他Core和你抢。怎么确认呢?和其他Core通个信,“我要修改这个内存,谁赞成,谁反对!“

但这样会不会很麻烦?

每一次内存读、写,都要互相通信,这通信次数、通信量也太大了。如何减少通信次数?

减少通信次数很简单啊,如果你已经取得了某一处内存(通常是64字节的一小块内存)的读、写权限,就可以不通信的情况下进行读写。

就好像,你把整条公路都买下来,公路就是你家庭院,你在自家庭院撒欢儿,就不需要看别人脸色了。

这是MESI协议的本质啊:

MESI协议的本质,在取得权限后、原先权限并无改变的情况下,再次进行权限对应的操作,无需核间通信。

先理解这个本质,接下来我们再来细节。

我们先说权限。

CPU对内存的访问,并不是以字节为单位,而是64字节为单位(有些CPU是128字节,国产的爱整这些小细节上的调整,以证明自己的技术实力)。

为什么不以字节为单位,因为CPU和内存间的延迟比较大吗。

就像送快递,大老远跑一趟,一次带上64件,万一除了目标这一件外,还有另一件也是要送的呢!

这个就是局部性原理,CSAPP那本书中有讲。(CSAPP:行业黑话,指《深入理解计算机系统》)

整个计算机从硬到软的体系中,到处都是这样的思想啊。数据库中你读写一行数据,数据库会将整个块(通常是8KB、16KB)读入内存,再进行读写。可以说数据库是以块(8K、16K)为单位读写的。这也是局部性原理。

CPU访问内存时的块大小,就是64字节。数据库的块大小是8K、16K……。

延迟越小时,块大小通常越小。延迟越大,块大小通常也越大。

稍具体的说一说,比如CPU要读、写char型(宽度为一字节)内存变量_x,

1.CPU先把_x所在64字节读入自己L1 Cache。每个64字节,在L1 Cache中,叫做一个Cache Line。

叫什么不重要,数据库中有叫块的,有叫页的。CPU这个块太小了,叫Line,仅此而已。不要在名字上纠结。

2.然后CPU再在刚才的L1 Cache Line中,读、写_x。

关于CPU和L1的关系,这里不展开了。咱们IT知识刺客后续文章,逐个讲解它们。

如果CUP要实现一个权限管理,自然不可能以字节为单位,每个字节都来个权限。这个64字节的Line,就是权限管理的目标。而权限管理的实现,就在L1中。

L1一共也就32KB(目前也有48KB或其他大小的,每提一个名词都要说一下特殊情况,也挺累的,而且一定有超出我了解的意外情况,后文我简化下,不这么搞了。我不是写技术清单,而是讲技术原理,各别地方没提到的很正常。我所讲的原理有问题的话,欢迎大家指正啊)。

一个Line 64字节,L1中一共也就512个Line。每个Line,多设置几个字节,做为状态标志。到底几个字节,我也不清楚。应该不会大,CPU里面可是不养闲人的,每个二进制位都要用到刀刃上。

我们先从最基本的开始,假设我在Line的状态标志中设置了读、写两个权限。并且制定如下规则:

1、CPU读、写L1 Cache Line前,先要得到对应的权限。

2、得到对应的权限时,要和其他Core通信,确认没有权限冲突时,才可以得到权限。

什么叫权限冲突?比如你要得到写权限,另一个Core已经在内存对应L1 Cache Line的状态标志上,拥有了写权限,而且还没有完成“写“操作。这就是权限冲突,这个时候,你要等。等对方发消息说:我好了,你可以继续了。

3、如果你的权限没有被改变时,进行同等、或低于此权限的操作,可以不进行通知。

第3点,是重点。前面讲了吗,MESI的本意,就是“减少核间通信的次数“,对应就是第3点。

我们稍详细解说下,先从权限改变开始。

你取得了写权限,在目标Cache Line对应的状态中,设置了“写权限“位。然后,你的写操作完成了。Cache Line状态中的写权限,会一直保留。

直到另一个Core说:“哥,我也想修改你的这个Line对应的内存“。这是另一个Core在请求写权限。

然后,你会把写权限降级为无权限。他会得到内存对应Line的写权限。

这个过程,你的权限被降级了。这就是权限改变。当然,写可以变为读、无权限。读也可以升级为写,或者无权限升级为写。等等。

明白意思就好,我们不是要设计CPU,是为了进行基础软件开发,没有必要把各个权限搞的非常细。

题外话,一直有人和我讨论基础软件开发,我常说,要有“基础软件思想“。为了有”基础软件思想“,当然要深究一些原理。要深究到哪一步呢?难到要从晶体管的PN结开始吗?

晶体管PN结还不是最基础的,是不是什么量子霍尔效应、量子隧穿(CPU有涉及量子隧穿)也要研究,都到这一步了,相对论铁定是绕不开了,这样一来,我一搞软件开发的,整成理论物理学家了。

关于原理层研究的“度“问题,你只能自己把握了。不排除你研究了好久,后来发现,自己的研究过了头。这也没关系,人生在世,谁能不走点弯路啊。不要纠结这个”度“的问题,关键是要尽早的开始你的研究,学过的东西,总会有用得着的时候。

说回第3点,如果你得到了写权限,别人也请求同一内存位置的写权限,你的写权限就改变了。

如果你得到了写权限后,其他Core没有请求同一内存位置的写权限,你的写权限就一直保留,一直是写权限。这个时候,你又对Cache Line发起一次写操作,或权限等级低于写的读操作,你不需要再和其他CPU通个信,可以直接向L1的Line写。这不就是减少了通信的次数吗。这就是MESI的本质意义。

就好像,你知道这是你自家的庭院,你可以随便走,不用担心撞到别人。

什么情况下我不用看人、随便走,什么情况下我要和别人通个信儿、打个着呼再走,把所有情况整理为一套规则,就是MESI。

具体而言,它把权限规定为4种:

M:Modify,修改权限。

E:Exclusive,独占权限。

S:Shared,共享权限。

I:Invalidate,无效权限。

其实这里的“权限”,换成状态,会更合适。修改、独占、共享、无效,四大状态。

四个单词首字母合起来,即MESI,也就是协议名称的由来。

对内存的操作,有Load、Store两种,也就是读、写了。

Store前,要把Line的权限状态先设置为E,独占。Store时,再转为M,修改权限。(这里有可能是是先M,再E,不影响我所描述的MESI思想,一些细节我记不太清了)

Load,当然对应S权限。但如果只有一个CPU Load,也会对应E独占权限。当其他CPU 也Load 同一内存处数据时,再由E降为S。

如果当前Core L1中某Line状态是M、E,这是高于S权限, Core可以直接Load。

如果你Store了,得到了M、E权限。别的Core Load了,你的M、E权限降级为S权限。你不再独占这块64字节的内存了,别的Core也读了这一块内存。

如果你Store了,得到了M、E权限。别的Core也Store,你的M、E权限降为I,无效权限。

如果你Load了,得到了E或S权限。别的Core Store,你的权限降为I。

也就是Store会将其他Core的无论什么权限,都降级为I。

I,无效权限,代表L1 Cache Line无效了,虽然它还在L1中,但CPU不能读它里面的数据了。因为它已经无效了吗。要读只能重新发起内存读、或从L2、L3读。

好,科普结束。

虽然说是科普,但学是说到了一些本质的,比如MESI的本质是“减少核间通信的次数”。为什么要减少核间通信的次数?因为核间通信延迟很高。

核间通信延迟,还有核间数据同步延迟有多高?

下面亲手来测量下吧。

“自己动手测量”可以把各种基础知识调动起来,把点连成线、把线连成面。然后,在某一天,你就会突然悟道,产生“基础软件思想”。

当然,这是我的方法,可能不一定适用于其他人。我写出来,仅供大家参考一下。

先考虑一个问题,如果CPU_a写一个内存,CPU_b多久能看到新值?(这是核间同步,它包括核间通信)

首先,我们得有一块共享内存。进程1、进程2各自在两个CPU上执行,进程1修改了一块内存,进程2读取同一内存,这不就会触发CPU的内部通信与同步吗!

所以我们要分配共享内存。

另外,还要控制进程在哪个CPU之上,因此我们还需要绑核,

分配内存,我用如下程序:

void * smem()
{ 
    void *ptr;
    ptr = mmap(0, (arr_size_X * arr_size_Y + 4 ) * sizeof(TYPE), PROT_READ | PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS|0x40000, -1, 0); // 大页
    if (ptr == MAP_FAILED)
    printf("Failed to allocate shared memory\n");
    return ptr;
}

MAP_SHARED标志声明这是一块共享内存。

0x40000标志是大页。

我的绑核函数:

void setcpu(int cpu)
{
    int i, nrcpus, cur_cpu;
    cpu_set_t mask;
    unsigned long bitmask = 0;
    
    CPU_ZERO(&mask);
    CPU_SET(cpu, &mask);
    
    if (sched_setaffinity(0, sizeof(cpu_set_t), &mask) == -1) 
    { 
        perror("sched_setaffinity fail!");
        exit(EXIT_FAILURE);
    }
} 

你看,试验还没开始呢,又是绑核、又是分配大页内存,是不是各种知识开始串起来了。

如果程序将来执行时,大页内存分配失败,可能是没有配置大页,可以如下配置:

cat /proc/sys/vm/nr_hugepages
echo 10 > /proc/sys/vm/nr_hugepages

我分配了10个大页,足够我们这个测量程序用了。

大页、绑核这些还能简单,关键是时间的测量。我们的测量目标是只有几十、几百周期的指令,如此精度的计时器,就只有TSC了。在《高性能计算(HPC)系列之二:深入基础软件开发》系列中,有使用过rdtscp,就是TSC。

TSC是核内一小块独立集成电路构成的计时器,它是独立的。它以基础频率为单位,它跟随频率高、低电平的变化而变化,每次高、低电平的转换,TSC加1。

也就是说,TSC就是以CPU基础频率为单位的时钟周期。

除了基础频率,CPU还有最小、最大频率。lscpu就能看到最小、最大频率:

[root@localhost ~]# lscpu
Architecture: x86_64
……
Model name: Intel(R) Core(TM) i5-6500 CPU @ 3.20GHz
Stepping: 3
CPU MHz: 3200.000
CPU max MHz: 3200.0000
CPU min MHz: 800.0000
……

最小频率是800,最大频率是3200。

由于睿频的存在,CPU的其他部件,在最小频率和最大频率间摆动。而我们的TSC是基础频率,这不同步啊。

不同步,一定会影响我们测量结果的精度。有时可能会得到远超预想值的测量结果。怎么办?

这好办,修改/sys/devices/system/cpu/intel_pstate/min_perf_pct,改为100:

echo 100 > /sys/devices/system/cpu/intel_pstate/min_perf_pct

这个意思就是让CPU一直跑在3.2GHz的最大频率下。

你可以用《高性能计算(HPC)系列之二:深入基础软件开发》中的频率测量方法,验证一下修改结果。

只要CPU的其他部件频率不再摆动,我们就可以用TSC进行测量了。

这里还有一个问题,多核间TSC的同步问题。

TSC是核内一块独立的模块不假,但每个核都有TSC,核间的TSC会不会不同步?

是有这个可能的。不过,Linux内核会定期同步所有核的TSC。所以,这个顾虑不用担心。而且如果你的CPU TSC就是不同步,像Intel,还提供了一个校准用的MSR寄存器。这一块我们后面单开一章来讲,其实用这个TSC还是能做很多事情的,比如生成分式数据库的ID什么的。

共享内存有了,绑核也搞定了,测量单位就是TSC:基准时钟周期。万事具备,开始设计程序吧。

我的程序思路如上图,两个进程,绑在两个核上,假设如图的CPU 0和CPU 1吧。

CPU 0的程序,一上来就是一个循环。直到读到mem_TSC0不为0,才退出循环。

mem_TSC0,从变量名上能看出来,它是一个内存变量(不能被编译器转为寄存器),它的内存就来自于共享内存。

它什么时候会不为0呢。要看CPU 1上的程序。

CPU 1上的程序一上来要先执行一个没什么意义的循环。目的,就是让CPU 0这边,先进入循环。

其实就是让CPU 1暂停一下,让CPU 0先走到循环里。

暂停,用sleep()可以吗?

不行。

因为sleep()会导致后续少量指令频率下降。如果两个CPU工作频率不一样,测量数据会不准确。

这里的暂停程序如下:

 if (cur_cpu == fg[2])
     for (i=0; i<=fg[3]; i++)
         k+=i;

对指定CPU循环执行k+i。

在CPU 0进入循环之后。

CPU 1使用rdtscp指令,得到当前TSC计数值。然后,写入共享内存变量mem_TSC0。

在写入之后,CPU 0还会在很长一段时间内,不断循环load mem_TSC0,并比较它的值是否不为0。

若干周期后,CPU 0读到的mem_TSC0终于不为0了,CPU 0上的循环退出。

CPU 0循环退出后马上再次调用rdtscp,得到TSC计数,写入mem_TSC1。

那么,mem_TSC1 – mem_TSC0,就是CPU 1写 共享内存mem_TSC0后,CPU 0多久时间才看到新值。

后面CPU 1的循环也一样,mem_TSC2 – mem_TSC1,是CPU 0写入新值,CPU 1多久后才能看到新值。

对应的代码如下:

// 将 TSC0, TSC1, TSC2 加载到两个Core的L1,最终Line状态为S
__asm__ __volatile__
(
    "lea    %0,     %%r8\n\t"
    "mov    (%%r8), %%rax\n\t" // TSC0
    "mov 128(%%r8), %%rax\n\t" // TSC1
    "mov 256(%%r8), %%rax\n\t" // TSC2
:
:"m"( *mem )
:"rax","r8"
);

// 暂停:第二个CPU上的进程暂停一下
if (cur_cpu == fg[2])
    for (i=0; i<=fg[3]; i++)
        k+=i;

// 开始测量
if ( cur_cpu == fg[1] )
{   // 第一个CPU中运行的测量程序
    __asm__ __volatile__
    (
        "lea    %0,     %%r8\n\t"             // &mem
        "SYNC_TSC0:\n\t"
            "mov    (%%r8), %%r9\n\t"         // while load
            "cmp    $0,     %%r9\n\t"
            "je SYNC_ TSC0\n\t"
        "rdtscp \n\t"                          // TSC 2
        "movq   %%rax,      128(%%r8)\n\t"
        "movq   %%rdx,      136(%%r8)\n\t"
        :
        :"m"( *mem )
        :"rax","rdx","r8","r9"
    );
}
else
{   // 第二个CPU中运行的测量程序
    __asm__ __volatile__
    (
          "lea    %0,     %%r8\n\t"       // &mem
          "rdtscp \n\t"                    // TSC 0
          "mov    %%rax,  (%%r8)\n\t"
          "mov    %%rdx,  8(%%r8)\n\t"
              "SYNC_TSC1:\n\t"
              "mov    128(%%r8), %%r9\n\t"     // while load
              "cmp    $0,        %%r9\n\t"
              "je SYNC_TSC1\n\t"
          "rdtscp \n\t"                  // TSC 3
          "movq   %%rax,      256(%%r8)\n\t"
          "movq   %%rdx,      264(%%r8)\n\t"
          :
          :"m"( *mem )
          :"rax","rdx","r8","r9"
    );
}

(完整程序我放在后面。这里只列出最重要的部分)

程序的运行结果:
 [root@localhost ff]# gcc -g mesi2.c -o mesi2
[root@localhost ff]#
 
[root@localhost ff]# ./mesi2 0 1 3 10000000
1-----------SUB Process at CPU: 1----------
PID is 23906 
TSC 0:416665567 0
TSC 1:416665785 218
TSC 2:416665998 213
2-----------SUB Process at CPU: 3----------
PID is 23907 
TSC 0:416665567 0
TSC 1:416665785 218
TSC 2:416665998 213

先说参数,./mesi2 0 1 3 10000000,0是在0号CPU上做初始化,分配共享内存、清0等等操作。1 3,两个进程分别在1号、3号两个CPU上运行。最后的10000000,是CPU 3上的程序,先执行10000000次循环,暂停一段时间。让CPU 1先进入循环。

好,看结果,TSC 1后面的218,就是CPU 3写mem_TSC0后,CPU 1在218个周期后才看到新值。

我们前面不是提过吗,CPU完成一个整数加、减法、位移、cmp等等,不涉及浮点数和乘、除,大部分指令都是一个周期。

一次L1 Cache访问,4、5个周期。

CPU 间的同步,218周期。

如果一次L1 Cache做为1天的话,那么:

加、减法 6个小时

L1 Cache访问 1天

CPU 间同步 1个半月

好了,先到这里吧。大家先消化消化,马上,《基础软件开发 -- 神秘的MESI和坑爹的LockFree》第二季就要发布了

标签:Core,--,TSC,LockFree,转帖,内存,Line,权限,CPU
From: https://www.cnblogs.com/jinanxiaolaohu/p/18010978

相关文章

  • SPI多个从设备问题排查
    记SPI问题排查 SPI电路挂载了2个从设备,S1和S2,其中S2前加了一个隔离器g,电路结构如下图:  问题发现:其它同事已经开发了S2驱动,我在开发S1驱动时发现S1无响应。 排查是软件问题还是硬件问题:检查代码SPI初始化(时钟等)片选信号的处理(多从设备做冲突处理)没有问题后S1还是无......
  • 【转帖】数据库传奇:MySQL创世之父的两千金My、Maria
    https://zhuanlan.zhihu.com/p/672142719 1人赞同了该文章《数据库传奇:MySQL创世之父的两千金My、Maria》一、前言  MySQL是一款备受欢迎的关系型数据库管理系统(RDBMS),最初由瑞典公司MySQLAB开发,目前隶属于OracleCorporation。在DB-Engines的排名中,MySQL稳......
  • THUPC2024-初赛
    哈哈,被干爆了。拖了cdqz哥后腿。题目使用协议来自THUPC2024(2024年清华大学学生程序设计竞赛暨高校邀请赛)初赛。以下『本仓库』皆指THUPC2024初赛官方仓库任何单位或个人都可以免费使用或转载本仓库的题目;任何单位或个人在使用本仓库题目时,应做到无偿、公开,严禁使用这......
  • 搞懂reshape
    作用:reshape是用来对多维数据进行重新排布的假设现在有一个(2,3,4)的Tensor。 如果但看2轴的数据,就是一个长度为4的向量。现在再看1轴和二轴的数据,表示有3个长度为4的向量,组成了一个二维Tensor[3,4],这个时候再来联系0轴,表示有2两个二维Tensor[3,4],依次这么往前推。现在假设r......
  • el-upload分片上传到七牛
    参考: https://blog.csdn.net/So_cute_girl/article/details/118606647需要自定义上传方法http-request,用七牛的分片上传<template><el-uploaddrag:limit="1"action="https://up.qiniup.com":multiple="false"accept=".mp4":d......
  • 北极网络
    不知道这道题目跟最小\(k\)度生成树有什么关系,到时候可以想一下不要一看到特殊点就想虚点,这道题目我们这么建模假设我们的\(D\)已经定了,我们把边权小于等于\(D\)的全部加入,那么图就会形成一个若干个连通块显然\(D\)越大连通块个数越少这里当然启示我们用二分,然而也有更简单的......
  • 给淘宝店铺接入第三方客服机器人需要花多少钱?
    在电商时代,淘宝卖家们都在追求高效率且低成本的运营模式。客服支出一直是我们店铺运营中一个不小的开销,同时快速响应顾客咨询又显得至关重要。许多卖家将目光转向了第三方GPT客服机器人,以期望通过自动化技术提高回复率并降低人力成本。然而,有的供应商一个店铺一年也就二三百,而有......
  • 43、excel快速填充序列号,删除行时序号自动跟上
    平时填充序号的做法:首先在第1、2行输入1、2,然后用手往下拖动,填充后面的行,缺点:当我删除一行时,后面的序号不会自动按顺序填充上 解决方法:1、在excel上选中A6单元格,然后左上角输入A6:A110,按【回车】键2、直接输入【=ROW()-1】,再按【ctrl+回车】键盘就可以了缺点:由于公......
  • 2024医保报销
    2024医保报销两定点三目录医保报销,先看“两定点三目录”它不是什么都给你报销,医保的使用,有着严格的执行标准,毕竟医保的盘子就那么大,想要报销还要熟悉里面的规则。两定点:“两定点”包括定点药店和定点医院,也就是买药要去定点药店才能给你报销,看病去指定的医院医疗费......
  • 后台没有调用分页方法,但是sql的执行却走了分页
    1、分页方法主要调用PageHelper.startPage()现象:开始是可以查询所有数据,在我调整前台代码以后,SQl查询就变为分页的了。因此,肯定是vue代码有问题,具体为啥前台代码会影响后台的分页呢?很是神奇。(计算机问题,表面上都有神秘的面纱哈哈哈)做法:(1)比对请求参数,发现比原来多了分页参数pa......