CPU 的基本结构

CPU 是一个计算机系统的核心,主要包括控制单元(指令计数器、指令跳转等)和逻辑计算单元(运算、比较大小等),CPU 通过北桥和南桥与外设相连。
image.png

  • 北桥:北桥是 CPU 和内存、显卡等部件进行数据交换的唯一桥梁,也就是说 CPU 想和其他任何部分通信必须经过北桥。北桥芯片中通常集成的还有内存控制器等,用来控制与内存的通信。现在的主板上已经看不到北桥了,它的功能已经被集成到 CPU 当中了。
  • 南桥:主要负责 I/O 设备之间的通信,CPU 要想访问外设必须经过南桥芯片。 总线是传输数据用的,它分为地址总线和数据总线。以内存为例,地址总线传输要访问的内存地址,数据总线传输读写的数据。有些总线地址和数据是分离的,有些是同一根总线分时利用。
  • FSB 总线:即前端总线(Front Side Bus),CPU 和北桥之间的桥梁,CPU 和北桥传递的所有数据必须经过 FSB 总线,可以这么说 FSB 总线的频率直接影响到 CPU 访问内存的速度。
  • ISA 总线:最早出现的标准总线,传输速度低,早期的低速外设会采用 ISA 总线进行连接,比如声卡。
  • PCI 总线:一种高性能局部总线,构成了 CPU 和外设之间的高速通道。显卡一般都是用的 PCI 插槽,PCI 总线传输速度快,能够很好地让显卡和 CPU 进行数据交换。

    CPU 的执行流程

    典型的 CPU 执行流程会包含以下 5 步:
  1. 取指:从内存中取出指令;
  2. 译码:识别指令的类型,计算指令长度,从指令中解析参数;
  3. 执行:将数据送给计算单元或者控制单元进行具体的计算和跳转;
  4. 访存:有些指令可能需要从内存加载数据;
  5. 写回:有些指令对寄存器或者内存状态有影响 ,将结果写入这些受影响的寄存器(比如:cmp 汇编指令会修改 EFLAGS 寄存器中对应的位)或者内存。

    涉及到与内存交互的步骤(1、4、5)往往容易成为性能瓶颈。

电路基础

组合电路最基础的门电路是由与门、或门以及非门,可以通过这几个最简单的门电路,组成形成更复杂的门电路,以实现更复杂的功能(比如 3 个与门,2 个非门,1 个或门可以实现简单加法器)。

固定的输入通过组合电路会得到固定输出。

但是,如果要实现一个累加器,仅使用组合电路是难以实现这样的功能的,因为需要一个能保存状态的东西来保存累加的结果。简单来说,就是需要将上一次累加的结果再反馈给电路作为下一次累加的输入。为解决这样的问题,需要引入 D 触发器(可以保存数据)以及时序电路。
单核主频是有极限的(更高的主频,导致更高的能耗),因此多核逐渐成为主流。
大核、小核的区别:大核频率高,电路更复杂,计算能力更强,功耗高;而小核反之。这是设计主要是为了适应不同特点的任务,有的任务时间敏感就调度给大核,有的任务时间长但性能要求不高就交给小核。任务的核间调度算法是大小核架构上的重要优化方向。

汇编语言与寄存器

X86中的常用寄存器

  • 16 个通用寄存器:RAX,RBX,RCX,RDX,RSI,RDI,RSP,RBP,R8,R9,R10,R11,R12,R13,R14,R15
    • RAX:调用程序时,用于存储返回值
    • RCX:在字符串处理指令中,常用做计数器
    • RSI:在字符串处理指令中,做为源操作数
    • RDI:在字符串处理指令中,常做为目标操作数
    • RSP:指向当前栈帧的栈顶
    • RBP:指向当前栈帧的栈基址
    • X86-64 位操作系统中,RDI,RSI,R8,R9 通常用于在调用函数时传递参数
  • 程序计数器:RIP
    • RIP 中记录着当前指令的地址,每次取指阶段完成以后就会指向下一条指令的地址,如果有办法修改这个寄存器的值,就可以控制程序的执行
  • 状态寄存器:EFLAGS
    • EFLAGS 中记录着溢出,方向,为零等状态。可以用于记录整个 CPU 的状态
  • 段寄存器:CS,DS,ES,FS,GS

    汇编指令与机器码的区别

    源代码:
    1. int main() {
    2. int i = 0;
    3. while (i < 10) {
    4. i++;
    5. }
    6. }
    使用 gcc -S 将 C 源码编译成汇编文件:
    1. main:
    2. .LFB0:
    3. pushq %rbp
    4. movq %rsp, %rbp
    5. movl $0, -4(%rbp)
    6. jmp .L2
    7. .L3:
    8. addl $1, -4(%rbp)
    9. .L2:
    10. cmpl $9, -4(%rbp)
    11. jle .L3
    12. popq %rbp
    13. ret
    使用 objdump -d 将源代码对应的可执行文件反编译出来:
    1. 00000000004004ed <main>:
    2. 4004ed: 55 push %rbp
    3. 4004ee: 48 89 e5 mov %rsp,%rbp
    4. 4004f1: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
    5. 4004f8: eb 04 jmp 4004fe <main+0x11>
    6. 4004fa: 83 45 fc 01 addl $0x1,-0x4(%rbp)
    7. 4004fe: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
    8. 400502: 7e f6 jle 4004fa <main+0xd>
    9. 400504: 5d pop %rbp
    10. 400505: c3 retq
    汇编指令和机器码的区别在于汇编指令是带符号标签的,可读性更强。在机器码中,数值是用补码表示的,使用补码的好处在于 +0 和 -0 有着相同的表示法以及减法运算可以复用加法的组合电路。

    汇编指令分类

    AT&T 与 Intel 汇编的区别主要有源操作数和目的操作数的顺序(AT & T 是左边为源操作数,右边为目的操作数)、AT&T 汇编的立即数需要加 $,寄存器需要加 % 以及表示操作数位宽的方式不同… ``` Intel:
    1. add eax, 1

AT&T: addl $1, %eax

Intel:
mov al, byte ptr val

AT&T: movb val, %al

  1. - 数据传送与算术运算
  2. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/21563208/1638605586967-6ba0bd99-d29c-4f22-a2a7-0450e6432bb7.png#clientId=ue6b5b090-a0c5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=251&id=ue8924179&margin=%5Bobject%20Object%5D&name=image.png&originHeight=502&originWidth=1327&originalType=binary&ratio=1&rotation=0&showTitle=false&size=208731&status=done&style=none&taskId=ud4d047e2-3ba5-4068-9062-defd22ab8fe&title=&width=663.5)
  3. - 位运算
  4. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/21563208/1638605880055-58607204-ff1f-4e9a-9f74-6ecdb90cf32d.png#clientId=ue6b5b090-a0c5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=273&id=u92b22244&margin=%5Bobject%20Object%5D&name=image.png&originHeight=546&originWidth=1324&originalType=binary&ratio=1&rotation=0&showTitle=false&size=196578&status=done&style=none&taskId=ud0dbf81f-6068-4332-9adf-859b2ae08b8&title=&width=662)
  5. - 条件分支
  6. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/21563208/1638605966230-a16474c2-0abc-424a-8ffb-a4b4d160fd49.png#clientId=ue6b5b090-a0c5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=274&id=JJLzv&margin=%5Bobject%20Object%5D&name=image.png&originHeight=547&originWidth=1328&originalType=binary&ratio=1&rotation=0&showTitle=false&size=289994&status=done&style=none&taskId=u5b3c8bc5-575f-446d-992e-7cf9cf6545d&title=&width=664)
  7. - 函数调用
  8. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/21563208/1638605937895-7e9e6c46-f3d5-4f25-8948-6cf5deacd0c2.png#clientId=ue6b5b090-a0c5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=287&id=u9d82fcdc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=574&originWidth=1326&originalType=binary&ratio=1&rotation=0&showTitle=false&size=350902&status=done&style=none&taskId=u05e8b36c-ab2f-4003-b524-9b7eb243a10&title=&width=663)<br />函数调用例子:
  9. ```cpp
  10. template <typename T>
  11. T dist(T x1, T y1, T z1, T x2, T y2, T z2) {
  12. T x = x2 - x1;
  13. T y = y2 - y1;
  14. T z = z2 - z1;
  15. return x*x + y*y + z*z;
  16. }
  1. # T 为 int
  2. 00000000004005dc <_Z4distIiET_S0_S0_S0_S0_S0_S0_>:
  3. 4005dc: 29 f9 sub %edi,%ecx
  4. 4005de: 41 29 f0 sub %esi,%r8d
  5. 4005e1: 41 29 d1 sub %edx,%r9d
  6. 4005e4: 0f af c9 imul %ecx,%ecx
  7. 4005e7: 45 0f af c0 imul %r8d,%r8d
  8. 4005eb: 42 8d 04 01 lea (%rcx,%r8,1),%eax
  9. 4005ef: 45 0f af c9 imul %r9d,%r9d
  10. 4005f3: 44 01 c8 add %r9d,%eax
  11. 4005f6: c3 retq
  12. # T 为 float
  13. 00000000004005f7 <_Z4distIfET_S0_S0_S0_S0_S0_S0_>:
  14. 4005f7: f3 0f 5c d8 subss %xmm0,%xmm3
  15. 4005fb: f3 0f 5c e1 subss %xmm1,%xmm4
  16. 4005ff: f3 0f 5c ea subss %xmm2,%xmm5
  17. 400603: f3 0f 59 db mulss %xmm3,%xmm3
  18. 400607: f3 0f 59 e4 mulss %xmm4,%xmm4
  19. 40060b: f3 0f 58 dc addss %xmm4,%xmm3
  20. 40060f: 0f 28 c3 movaps %xmm3,%xmm0
  21. 400612: f3 0f 59 ed mulss %xmm5,%xmm5
  22. 400616: f3 0f 58 c5 addss %xmm5,%xmm0
  23. 40061a: c3 retq

区别在于使用的寄存器不一样以及用于传递函数返回值的寄存器也不一样,且下面使用的汇编指令为专门用于浮点数计算的指令

  • 字符串处理

image.png

中断的基本原理

image.png

流水线与指令调度

典型的 CPU 执行一条指令时通常会包含 5 个阶段,而最简单的 CPU 执行方式就是循环执行这 5 个阶段。 但实际上,这 5 个模块是相互独立的,比如当译码模块在工作的时候,取值模块仍然可以工作,它可以取下一条指令进来。因此,可以使用流水线技术实现指令级并行。

编译器指令调度

image.png
image.png

第三行笔误,应该是 c=> R3

CPU 乱序与投机执行

CPU 会使用乱序窗口(ReOrdering Buffer,ROB)一次性取多条指令,并判断指令之间是否存在数据依赖、结构冲突,如果没有依赖和冲突的指令的话,就可以一起发射。CPU 对分支指令进行预测,对于可能会执行的分支可以提前投机执行。如果预测错误,执行结果就不 commit,如果预测正确,性能会有很大的提升。

相关阅读:gcc内建宏likely, unlikely

对于分支预测错误带来的危害,不仅会由于流水线的清空导致的几个指令周期的时间损失,更重要的是由于指令的 cache miss 而带来的更大的损失。
因此指令的乱序执行不仅受编译器指令调度的影响,还受 CPU 乱序执行的影响。

CISC VS RISC

image.png
image.png

指令寻址

image.png

括号类似于对地址进行解引用

image.png

C++ 内存布局

类成员的字节对齐

  1. class A
  2. {
  3. public:
  4. int a;
  5. char b;
  6. short c;
  7. };
  8. int main()
  9. {
  10. A* obj = new A();
  11. obj->a = 1;
  12. obj->b = 2;
  13. obj->c = 3;
  14. return 0;
  15. }

反汇编结果如下:

  1. e8 f1 fe ff ff callq 400510 <_Znwm@plt> // 调用 new
  2. c7 00 00 00 00 00 movl $0x0,(%rax) // 成员 a 赋值为 0
  3. c6 40 04 00 movb $0x0,0x4(%rax) // 成员 b 赋值为 0
  4. 66 c7 40 06 00 00 movw $0x0,0x6(%rax) // 成员 c 赋值为 0,注意由于字节对齐, c 的地址为 0x6(%rax)
  5. 48 89 45 f8 mov %rax,-0x8(%rbp) // 返回值赋值给栈上的 obj
  6. 48 8b 45 f8 mov -0x8(%rbp),%rax
  7. c7 00 01 00 00 00 movl $0x1,(%rax) // 设置成员 a 的值
  8. 48 8b 45 f8 mov -0x8(%rbp),%rax
  9. c6 40 04 02 movb $0x2,0x4(%rax) // 设置成员 b 的值
  10. 48 8b 45 f8 mov -0x8(%rbp),%rax
  11. 66 c7 40 06 03 00 movw $0x3,0x6(%rax) // 设置成员 c 的值

虚函数

  1. class A {
  2. public:
  3. int a;
  4. virtual void foo() {}
  5. };
  6. int main() {
  7. A* obj = new A();
  8. obj->a = 1;
  9. return 0;
  10. }

反汇编结果如下:

  1. 48 83 ec 08 sub $0x8,%rsp
  2. bf 10 00 00 00 mov $0x10,%edi
  3. e8 e2 ff ff ff callq 400580 <_Znwm@plt> // 调用 new
  4. 48 c7 00 70 07 40 00 movq $0x400770,(%rax) // 设置虚表指针
  5. c7 40 08 01 00 00 00 movl $0x1,0x8(%rax) // 设置成员的值
  6. 31 c0 xor %eax,%eax
  7. 48 83 c4 08 add $0x8,%rsp
  8. c3 retq

继承

  1. class A
  2. {
  3. public:
  4. int a;
  5. virtual void foo()
  6. {
  7. printf("In A\n");
  8. }
  9. };
  10. class B : public A
  11. {
  12. public:
  13. int b;
  14. virtual void foo()
  15. {
  16. printf("In B\n");
  17. }
  18. };
  19. int main()
  20. {
  21. A* obj = new B();
  22. obj->a = 1;
  23. obj->foo();
  24. return 0;
  25. }
  1. mov -0x18(%rbp),%rax // 返回值赋值给栈上的 obj
  2. movl $0x1,0x8(%rax) // 设置成员 a 的值
  3. mov -0x18(%rbp),%rax
  4. mov (%rax),%rax // 对 obj 解引用,获得虚表指针
  5. mov (%rax),%rax // 对虚表指针解引用,获得 foo 的地址
  6. mov -0x18(%rbp),%rdx
  7. mov %rdx,%rdi // 将 obj 作为函数形参
  8. callq *%rax // 调用 foo

编译单元

  1. 每个独立的 C/Cpp 文件是一个编译单元。不在本编译单元的符号要使用 extern 来声明,不需要对外暴露的符号尽量使用 static 来修饰。
  1. class A {
  2. public:
  3. template <typename T>
  4. virtual T add(T a, T b) {
  5. return a + b;
  6. }
  7. };
  8. int main() {
  9. A a;
  10. return 0;
  11. }
  12. // error: templates may not be ‘virtual’

为什么函虚数不能使用泛型声明?因为每个编译单元独立编译,假设在一个工程中有多个编译单元都实例化了该类,假设编译单元 A 实例化了 int,float 两种对象(对应虚表的大小为 2),而编译单元 B 只实例化了 int 这一种对象(对应虚表的大小为 1),那么编译器在编译这两个编译单元时怎么确定虚表的大小呢,只能遍历所有的编译单元来确定虚表的大小,这对编译器的实现上来说是非常低效的。

一个类只维系一张虚表!!