暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

详解超线程:一点也不逻辑的逻辑CPU

IT知识刺客 2024-08-01
169
看到知乎一个问题,CPU线程数和核心数有什么关系。
CPU线程数,这是一个专有名词,也可以叫“CPU超线程数”。我怀疑题主把它和操作系统线程搞混了。都是线程,所谓一笔写不出俩线程,还的确挺容易搞混的。
超线程吗,就是CPU内的多线程呗。其实,真相并不止于此。
操作系统线程,你可以使用pthread_create()创建,man一下:
    [root@oracledb ~]# man pthread_create
    就能看到相关说明和简单的例子。我们不详述了,这个基本都懂。我们后文称pthread_create(或类似函数)创建的,为“操作系统线程”。
    而CPU的线程数,因为也有“线程”二字,它好像也可以指操作系统线程,但更准确,它就是指CPU的超线程,也是我们常说的逻辑CPU。
    一般CPU超线程数为2,也可以说CPU的线程数为2,就是一个物理Core,又可以当两个逻辑Core来用。在一些资料中,也把超线程为2,称为CPU线程数为2。
    可是,别看人家叫“逻辑Core、逻辑CPU”,可一点都不“逻辑”,人家可是正儿八经“物理”的。
    此话怎讲呢?看下图:

    图1

    我把CPU中的“计算单元”等内部组件比作图1的办事大厅,办事大厅有个对外的窗口,大家排着队,在窗口这儿办事。

    注:

            计算单元是啥,先不必计较,就是CPU内完成计算、逻辑等的物理电路部分。


    “办事”的时候,只能一个一个的来,两个人不能在一个“办事窗口”同时办事。

    如果后面办事大厅中工作人员的时间允许,允许两个人同事办事,要怎么办呢?


    图2
    如图2所示,很简单,再多开一个窗口。
    对于CPU来讲,和程序直接对接的,首先是那些个通用寄存器,比如rax、rbx、……,包括状态寄存器eflags等等。
    还有SIMD(Intel CPU)的寄存器,xmm0~15/ymm0~15/zmm0~15。
    这些,都相当于“窗口”。
    注:
        SIMD寄存器是包含关系,比如zmm0的低256位是ymm0,低128位是xmm0。

    增加一个窗口的成本是很高的,像SIMD寄存器,都很大,如果是AVX512,一个zmm寄存器512位,64字节。16个zmm寄存器还挺大的。
    而且,除了对外的窗口(通用寄存器),办事大厅内部,也要进行些物理改造。
    这个就有点太偏CPU底层了,我简单说下,你有个大概的印象就好。比如CPU内,内存的读、写都要放在内存排序区中。从内存读的指令,被放在Load Buffer中(简称LB,LB队列),向内存写的指令,被放在Store Buffer中。
    (Store Buffer的简称是啥,猜一猜。
    ……
    对,你猜的没错,就是SB,傻B队列
    因为SB、LB中的读写要排序,两个程序的读、写,没必要一起排序,各排各的就好。简而言之,一句话:两个对外的“窗口”不能共享一个SB、LB。SB、LB也要准备两套。
    等等吧,除了通用寄存器、SB/LB两队列,还有些其他的内部组件,要分开。
    总之,就是前面的窗口要两套、里面有些集成电路,也要分成两套。
    所以,逻辑Core,还真不是逻辑。人家是有真东西的,是真要在Core中加上一堆物理的集成电路,才能把一颗Core,当两颗用。
    你可以理解为,一条公路本来只能一个人用,现在要允许两个人同时用,那就要装个红绿灯啥的,以免两个人起冲突。
    所以,超线程出的逻辑CPU,并不是纯“逻辑”,人家是有真东西的。
    说到这儿再和操作系统线程做个简单的对比:
    图3
    图3相当于操作系统线程,每个人在一个窗口前办事儿。每个人10厘秒,无论事儿办完没办完,轮到下一个人办。
    这其实也部分解答了另一个问题,“操作系统线程数可以很多,为什么CPU的超线程数不能太高?”
    通常超线程数都是2,也就是一个物理Core变两个逻辑Core。为什么不能多搞点?
    因为和操作系统线程不一样。从现代CPU的角度上说,操作系统多线程是纯软的,是没有成本的(当然,线程数太多影响性能)。CPU线程数是需要在CPU中增加额外的集成电路的,是有成本的。
    而且,CPU就那么大块地方,“办事窗口”和LB/SB占的地方多了,计算单元、分枝预测、各级Cache 等等,能占的地方就少了。这在经济学中有一个词:机会成本。
    CPU超线程数是要考虑机会成本的。
    那么,对于目前为2的超线程数2来说,机会成本是否值得?
    要回答这个问题,先要搞清楚CPU超线程的主要目的是啥!
    CPU在运行时,常会因为一些事情被阻塞,这里通常用术语STALL表示CPU流水线阻塞。
    造成STALL的原因,最常见如访存。
    一次读、写内存,通常需要100到500个时钟周期。而每个时钟周期,都可以有一到多条基本运算完成。比如整型数的加、减法,可以认为只需要一个时钟周期。

    注:

        CPU是流水线的,每个时钟周期都可以有1条、甚至4或更多条基本运算完成。但一条基本运算并不能在一个时钟周期内完成。


    打个比方吧,普通的指令,相当于在一条直路上跑,if/swith/while这些,相当于拐弯,访存,相当于等红灯。
    CPU在执行某个进/线程指令时,遇到Cache Miss,发起访存。当访存过多时,就会STALL。
    在STALL期间,这个进/线程还在运行中,别的进/线程并不能使用这颗CPU的资源。不能因为我访问了次内存,就把我切出CPU,是吧。
    但因为访存的STALL,又会让CPU一下子空闲好久,这不是有点浪费吗,CPU的超线程技术就出现了。
    为一颗CPU添加“窗口”、LB/SB等电路,但共享后面的计算、逻辑等单元,一颗CPU变两颗CPU,一颗CPU在STALL时,另一颗CPU可以使用全部的计算、逻辑等单元。
    如果两颗逻辑CPU都不STALL,计算、逻辑等单元就分开,一人一半。

    注:

        实事上,超线程CPU中,各种计算、逻辑等单元组件被分为静态单元与动态单元,静态单元只属于某一逻辑Core,即使它Stall,同一物理Core的另一逻辑Core也不能使用这些静态单元。动态单元就是两个逻辑Core间动态分配


    这样,CPU后端的计算、逻辑等单元的利用率,会大大提升。
    但在没有STALL时,毕竟计算、逻辑等单元要被分给两个逻辑CPU,性能还是会有所下降的。
    好,问题都解答完了,再延伸一下,我们的程序、软件,到底需不需要超线程?
    这个问题其实挺容易回答的,超线程是当有一个逻辑Core Stall时,那些在两个逻辑Core间动态分配的计算单元等,不会被浪费,另一个逻辑Core可以使用这些动态单元。
    如果一段程序,由于频繁的访存(L1/L2 Cache Miss)等原因,造成STALL很多,那超线程就是有必要的。
    如果程序的STALL很少,超线程的作用就不大,还会起到反作用。
    这里需要注意,超线程是否有助益,是取决于程序、软件的。
    但问题是,我怎么知道我的程序/软件,STALL多不多?
    太简单了,上perf。
    Perf中为我们准备了非常多的性能数据,你可以“perf list”看一看:
      [root@oracledb ~]# perf list


      List of pre-defined events (to be used in -e):


      branch-instructions OR branches [Hardware event]
      branch-misses [Hardware event]
      bus-cycles [Hardware event]
      cache-misses [Hardware event]
      cache-references [Hardware event]
      cpu-cycles OR cycles [Hardware event]
      instructions [Hardware event]
        ref-cycles                                         [Hardware event]event]
      ……
      这里面有好多来自于CPU的硬件计数器(也称性能计数器),所以CPU不同,数量也不同。
      像Intel/AMD,CPU内置的计数器多达数百个,可以提供近1000项性能资料。
      刚才不是说CPU内空间有限,各种组件集成电路规模多寡,都是有机会成本的吗,这么多计数器,也是有机会成本的。但这些顶级的CPU厂商,都认为性能计数器的机会成本是值得的。
      使用这些性能计数器,可以对程序进行Profiling,这个词叫它“画像”吧。
      如果一段程序是性能敏感性质的,使用CPU内性能计数器,可以对程序的运行状态,进行全方位的“画像”,了解它在CPU上运行时的真正情况。并在画像基础上,进一步优化。
      这往往可以使程序性能至少提升10%以上,多数程序的性能可以提升30%至100%。
      所以,在CPU中内置这么多计数器,让CPU“可观测”,其机会成本,是相当值得的。
      使用这些CPU硬件性能计数器,还有一个作用,可以观测程序到底写的好不好。
      好了,先不展开了,回到我们的主题,以下几个性能计数器,可以让我们一窥CPU运行时的STALL:
      cycle_activity.stalls_total:首先,最直接的一个,就是此计数器。stalls_total,这已经告诉我们了,它是衡量总的STALL周期数的。严谨点说,它是记录执行阶段流水线的STALL时钟周期数(忘了这句话吧,就当它是统计总STALL周期数的,也没任何问题)。
      cycle_activity.stalls_mem_any:它可以统计因内存而产生的STALL周期数。虽然这也没问题,但我还是严谨点说下吧,其实这个计数器的原理是这样的,在流水线进入执行阶段时,如果有某个时钟周期没有任何一条uOP被运行(即STALL),并且,有正在等待的内存访问(包括L1/L2/L3,和内存),那么此计数器就加一。同上,“严谨点说”这一段,你可以忽略。
      有人认为“访存”,是所有程序最主要的STALL原因,还创造了内存墙这个词。但对很多程序来说,还会有其他STALL的原因。
      通过计算stalls_mem_any在stalls_total中的比例,可以让你感受下内存墙影响到底有多大。
      Instructions:指令数。这个很简单,我去年曾用这个比较各个数据库在相同数据量下、执行相同SQL时的指令数。并总结为一个词:“油耗”。
      毕竟,同一逻辑的程序,如果Linux之神Linus用100行,你用1000行,你的油耗太高了。就像汽车,开同样10公里,你的油耗比Linus高10倍的话,你的车不如Linus(你写的代码不如Linus)
      比较的结果,各个国产数据库的油耗遥遥领先。其中最引人注目的,就是华为高斯,清场式领先。一会,你会再看到这个比较。
      cycles:周期数。即,时钟周期数。如果主频是4Ghz,4个时钟周期1纳秒。一个时钟周期0.25纳秒。
      好,下面开始我们测试。
      我的测试目标,是OLTP型数据库应用,我以一条简单的SQL为例:select * from vage2 where id1=?
      ID1列为主键,表有300万行。
      测试方式如下:
      步1:执行perf命令:
        perf stat -einstructions:u,cycles:u,cycle_activity.stalls_total:u,cycle_activity.stalls_mem_any:u -p 28477
        其中 28477 ,为进程号。如果是MYSQL、华为OpenGauss,这里要改为“-t 线程号”。
        “:u”,代表只在指令是用户态时才进行计数。其实就是去除操作系统的影响。
        步2:在28477进程的Session中,执行SQL:
          postgres=# execute cs(1);


          id1 | id2 | c1 | c2
          -----+-----+----------+---------
          1 | 101 | AAAAA111 | aaaaaa1


          (1 row)
          注:SQL为:
            prepare cs(int) as select * from vage2 where id1=$1;
            并且已经执行execute cs(1)多次。我是统计Cache Plan的效果。因为不使用Cache Plan的话,指令数等太多,很多指标会更差。
            步3:回到perf中,Ctrl+C,就能看到效果了:
              [root@oracledb ~]# perf stat -einstructions:u,cycles:u,cycle_activity.stalls_total:u,cycle_activity.stalls_mem_any:u -p 28477
              ^C <-------按Ctrl+C
              Performance counter stats for process id '28477':


              60,054 instructions:u # 0.17 insn per cycle
              357,168 cycles:u
              302,468 cycle_activity.stalls_total:u
              122,362 cycle_activity.stalls_mem_any:u


                     1.007995611 seconds time elapsed
              顺带统计Oracle的:
                [root@oracledb ~]# perf stat -einstructions:u,cycles:u,cycle_activity.stalls_total:u,cycle_activity.stalls_mem_any:u -p 1350
                ^C
                Performance counter stats for process id '1350':




                63,792 instructions:u # 0.18 insn per cycle
                344,998 cycles:u
                296,039 cycle_activity.stalls_total:u
                146,859 cycle_activity.stalls_mem_any:u




                1.155029629 seconds time elapsed


                还有遥遥领先的国产数据库:

                  [root@localhost ~]# perf stat -einstructions:u,cycles:u,cycle_activity.stalls_total:u,cycle_activity.stalls_mem_any:u,L1-dcache-loads:u -t4504
                  ^C
                  Performance counter stats for thread id '4504':




                  262,662 instructions:u # 0.15 insn per cycle
                  1,681,672 cycles:u
                  1,411,605 cycle_activity.stalls_total:u
                  614,187 cycle_activity.stalls_mem_any:u




                  1.959583056 seconds time elapsed

                  我把数据都写进一个表格:


                  PG15.7

                  Oracle  11.4

                  Open

                  Gauss 5.0

                  Instructions

                  60,054

                  63,792

                  262,662

                  Cycles

                  357,168

                  344,998

                  1,681,672

                  cycle_activity.stalls_total

                  302,468

                  296,039

                  1,411,605

                  cycle_activity.stalls_mem_any

                  122,362

                  146,859

                  614,187

                  高斯不出意外的遥遥领先,相同数据量、相同逻辑的SQL,单从指令数上,从原生PG多4倍。
                  这是使用了CachePlan的结果,否则,高斯的指令数比原生PG高7倍以上(这说明高斯把很大精力花在优化器上)。
                  PG和Oracle,则相差不多,分别使用了35万多、34万多个时钟周期。talls_total呢,多达30万周期上下。85%的时钟周期,都在STALL。
                  高斯虽然指令数多、周期多,但单从STALL周期数上看,也是85%左右的时钟周期在STALL(准确是83.9%)。
                  三种数据库在执行一条简单OLTP型SQL时,STALL周期都占总周期数的80%以上。
                  就像上班时,你的80%以上时间,都在因为等待一些资源,而是空闲状态。提高效率的方法只有一个吗,再给你加点工作,让你在等待资源时、可以去忙别的工作。
                  前面用1000字左右,介绍了perf,现在回到我们的主题,还记得前面的结论吗,CPU的超线程,是为了在STALL期间,让另一个逻辑Core可以有效的利用CPU内的硬件资源。你看数据库的STALL这么多,所以对于OLTP型小SQL来说,超线程是有利于提升CPU内资源利用率的。
                  再展开点说一下,stalls_mem_any在stalls_total中的占比,Oracle勉强50%,PG只有40%,高斯是43%。
                  所以说,对于OLTP SQL,所谓“内存墙”,也并不是那么的高。
                  好了,此问题就到这里吧。
                  最后,好多人都说就算对数据库来说,超线程也是有害无益,这好像跟我测试的结果相反,真实原因,是现在的数据库内核,并没有考虑CPU层级的硬件特性,纯靠编译器和操作系统,并不能发挥出超线程等CPU内特性的真正潜力。

                  文章转载自IT知识刺客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论