yuenshome / yuenshome.github.io

https://yuenshome.github.io
MIT License
84 stars 15 forks source link

CPU 怎么寻址的 #140

Open ysh329 opened 2 years ago

ysh329 commented 2 years ago

本文主要讲内存寻址方式,以及各种语言的内存模型。来自华为——海纳,编译器专家《CPU怎么寻址的》。

ysh329 commented 2 years ago

1. 概念和历史

image 似乎当前华为编译器总负责人也是Patterson的学生。

2. CISC vs. RISC

image 复杂指令集典型例子如 x86 架构,精简指令集典型是 RISC 架构,比较如下:

CICS Intel 指令的 手册长达 1200 多页,详细且复杂,而 RISC 只有100多个,有的指令长度是 1 个字节,而有的是 2 个。相对来说,RISC 编码长度都是固定长度,好处是其 CPU 的 Verilog 实现,取址的指令长度都是固定的,比方每次 PC (PC:Program Counter,是通用寄存器,但是有特殊用途,用来指向当前运行指令的下一条指令)都 += 4,对于 CPU 设计人员更喜欢精简的,相比 CICS 也比较省电路面积。

CICS 的指令操作方式多种多样,先学 x86 再看 Arm 会觉得简单一些, Arm 太简单了,有些概念放到 x86中比较奇怪。

CISC 和 RISC 是一种比较简单粗暴地划分,并非 RISC 就不能有条件码,此外 RISC 的特点,在能省电路面积的地方就生下来,这也是其 tradeoff 。

Arm 和 x86 相比,前者定长,也就意味着有些指令原本可以压缩,比方都是4字节,但是没必要比方原本可以1/2字节就可以,从霍夫曼编码的角度来讲,1/2字节的编码尽可能多就会生更多空间。

通常来讲,x86 一条指令就能搞定,但是Arm 上需要4、5条指令, Arm 比较精简,功耗小。随着整个物联网的发展,RISC 的发展也很迅速。

image

上面的“条件码”,指的是 CPU 根据运算结果由硬件设置的位,体现当前指令执行结果的各种状态信息。例如:算术运算产生的正、负、零或溢出等的结果。条件码可被测试,作为分支运算的依据,此外,有些条件码可被设置,例如对于最高位进位标志C,可用指令对它置位和复位。

ysh329 commented 2 years ago

3. x86 与 Arm 对比举例:CAS

CAS看起来像是 compare and exchange 或者 Compare And Swap 或者的简称。

下面涉及到内联汇编,其通常嵌入在 C/C++。从下面代码中__asm__ volatile开始,先说参数部分:

对于汇编代码,其中的%1是exchange value,计第二个冒号后的第一个参数,%3是dest指向的地址值。这段代码可以理解为:用cmpxchgl指令做一个比较,当compare value和 dest 对应的值相等时,会做值交换。%4是mp,当多线程时,则会执行。其实这里的代码贴的不完整,没有用到第二个参数,过程有些问题。

虽然这段汇编代码可能存在问题,不重要了。这部分主要是想表达,在这里 x86 的 CISC 一条指令 cmpxchgl 就可以完成这个 Compare And Swap 功能

image

作为与 CISC 的对比,Arm 的 CAS 实现就较复杂,见下图代码中的__asm__部分中,有调用到ldrexteqmovstrexeq指令,其流程是将%2里的值加载到%1,然后做teq等待操作。

image

总之,相比 x86 的 1 条指令完成 CAS ,Arm 需要 4 条指令完成同样的操作。

ysh329 commented 2 years ago

4. 指令中的操作数的寻址方式

image

image

如果用objdump反汇编,则可以看到第三个比例变址寻址是上面的两个变址寻址的通用形式。最后的那种:变址寻址加上立即数,已经是最复杂的形式了。

带括号就是取地址,括号里有数的话就是比例。

关于%:Intel中用Visual Studio,不带%的,多是Intel的语法,而AT&T则是寄存器,则是带%。

ysh329 commented 2 years ago

5. C++内存布局

a是4、b是1、c是2

汇编代码

objdump,可以查看内存布局

image

ysh329 commented 2 years ago

5.1 C++内存布局:虚函数

0x400700是一个虚表,

调用a所指向的foo,需要查到 image image

ysh329 commented 2 years ago

5.2 C++内存布局:继承

image

5.3 运行时识别

RTTI,运行时,读取内存中的虚表。经过两次指针访问,得到函数指针

image

ysh329 commented 2 years ago

5.4 dynamic_cast 依赖虚表

如果下面没有红色的Virtual,即右边的代码,编译时会报错:不是多态,不能进行 dynamic_cast,

转换过程实际是:取出 A 的虚表和 B 的虚表,如果不相等,则转换不了,因为不是同一个类型。 Virtual 不在的话,就不存在虚表,无法进行 dynamic_cast。

image

5.5 理解编译单元

image

ysh329 commented 2 years ago

6. Java对象内存布局

Klass*:记录各个field的名字,父接口,父类,Interface,继承自哪个类等等,均在Klass结构中;

oopmap:Java的每个属性可以是引用,int、double,如果是int、double就不需要啥操作了,在GC时,对于判断是引用还是数字则通过oopmap。Klass还有反射、查找函数、gc等等结构。

比方在Java中定义了A,A的每个实例a都是指向A的Klass。如果问Java是否都存在虚表,那就是了,即Klass vtable。Java中所有的函数天然地、全部地都是虚函数。 image

ysh329 commented 2 years ago

7. Python对象内存布局

image

key是属性名,value是属性值。运行时,就可以删掉属性, 但是Java不行,Python存在一张hash表,其中带了属性。Python虽然占用内存且效率低,但运行时可以随时增删属性,此外,其类也是可以随时添加删除属性,表现地过度灵活。

对于JS v8,介于Python与JS之间,JS也可以增删属性,但是JS没有按照Python这样hash表来做。而是先创建8field的空间,增删时就用这8个field,当gc时就可以合并,此外这个过程也用到了隐式类。这方面更深入的可以看JS Inner Class。

本节课通过介绍CPU的寻址方式,介绍C++的3个语言特性:虚函数,dynamic cast,为什么虚函数不能是多态的。此外还有很多。

8. 内存管理总结

  1. 内存知识是基石。对于深入理解各种语言,广泛而普遍;
  2. 系统级别开发人员缺口大,职业天花板高,走出内卷;
  3. 内存知识零散且难。每学一个知识都需要太多的前置知识,看不到边界。