《深入浅出计算机组成原理》学习笔记1——计算机基本组成与指令执行

一丶冯·诺依曼体系结构:计算机组成的金字塔

1.从装机的角度看计算机基本组成

  • CPU:

    计算机最重要的核心配件,全称中央处理器,计算机的所有"计算"都是由 CPU 来进行的

  • 内存

    撰写的程序、打开的浏览器、运行的游戏,都要加载到内存里才能运行。程序读取的数据、计算得到的结果,也都要放在内存里。内存越大,能加载的东西自然也就越多

  • 主板

    CPU 要插在主板上,内存也要插在主板上。主板的芯片组(Chipset)和总线(Bus)解决了 CPU 和内存之间如何通信的问题。芯片组控制了数据传输的流转,也就是数据从哪里到哪里的问题。总线则是实际数据传输的高速公路。因此,总线速度(Bus Speed)决定了数据能传输得多快

  • I/O 设备

    输出设备:如显示器

    输入设备:如鼠标、键盘

  • 硬盘

    数据持久化保存

2.冯·诺依曼体系结构

冯·诺伊曼体系结构(Von Neumann architecture),也被称作冯·诺伊曼模型,是由匈牙利数学家和物理学家约翰·冯·诺伊曼在1945年提出的一种计算机组织架构。它是现代计算机设计的基础,对计算机科学和技术的发展产生了深远的影响。以下是冯·诺伊曼体系结构的主要内容:

  1. 处理单元

    • 中央处理单元(CPU):负责解释和执行程序指令,以及控制和处理数据。
    • 运算逻辑单元(ALU):执行所有的算术和逻辑运算。
    • 控制单元(CU):解释程序指令并控制其他部件按顺序执行这些指令。
  2. 存储单元

    • 内存:用于存储正在执行的程序的指令和数据,以及程序运行的中间结果。
  3. 输入设备

    • 用于将外部数据传输入系统。
  4. 输出设备

    • 用于将数据和计算结果从系统传输到外部。
  5. 存储程序

    • 程序指令和数据都存储在同一个读写内存中,CPU从内存中读取指令执行,这就是"存储程序"的概念。

冯·诺伊曼体系结构的牛逼之处在于:

  1. 简化了计算机设计

    通过将程序指令和数据都存储在同一内存中,冯·诺伊曼体系结构简化了计算机硬件的设计。这一点与早期的计算机设计相比,如哈佛架构,它将指令内存和数据内存分开,冯·诺伊曼的设计大大简化了信息流和控制流。

  2. 灵活性和通用性

    存储程序的概念使得计算机能够通过加载不同的程序来执行不同的任务,这增强了计算机的灵活性和通用性,使同一台机器可以在多种场景下使用。

  3. 促进了软件发展

    因为程序是以软件形式存在的,软件开发者可以编写和修改程序而不需要改变硬件,这促进了软件行业的发展和软件技术的创新。

  4. 便于理解和编程

    冯·诺伊曼体系结构提供了一种清晰的计算模型,使得计算机科学家和工程师能够容易地理解和设计计算机系统,同时也为编程语言的发展和程序员的编程提供了便利。

尽管如此,冯·诺伊曼体系结构也有其局限性,比如著名的"冯·诺伊曼瓶颈",即CPU和内存之间的数据传输速度限制了计算机的性能。现代计算机设计仍然基于冯·诺伊曼架构的基本原则,但采取了多种措施,如使用缓存、多核处理器、流水线技术等,来克服这些局限性。

二丶理解性能

1.响应时间&吞吐量:

  • 响应时间:

    执行一个程序,到底需要花多少时间。花的时间越少,自然性能就越好。

    随着摩尔定律的失效,cpu的性能提升逐渐变缓。

  • 吞吐率或者带宽:

    一定的时间范围内,到底能处理多少事情。这里的"事情",在计算机里就是处理的数据或者执行的程序指令。

    通过增加cpu核心数,实现人多力量大,多个核同时处理数据,单位时间内能处理的时间也就更多。

2.cpu时钟

  • 程序的 CPU 执行时间 =CPU 时钟周期数×时钟周期时间

  • CPU 时钟周期数

    CPU 内部,有一个叫晶体振荡器的东西,简称为晶振。晶振带来的每一次"滴答",就是时钟周期时间。在我这个 2.30GHz 的 CPU 上,这个时钟周期时间,就是 1/2.30GG。我们的 CPU,是按照这个"时钟"提示的时间来进行自己的操作。主频越高,意味着这个表走得越快,我们的 CPU 也就"被逼"着走得越快。

但是提升cpu时钟周期数也就是提升主频,意味着换一个更好的cpu。进一步看程序执行耗时时间:CPU 时钟周期数

CPU 时钟周期数=指令数×每条指令的平均时钟周期数

因此 程序的 CPU 执行时间 = 指令数×每条指令的平均时钟周期数x 时钟周期时间

优化程序性能其实就是优化这三者

  • 时钟周期时间,就是计算机主频,这个取决于计算机硬件。
  • 每条指令的平均时钟周期数 CPI,就是一条指令到底需要多少时钟周期。现代的 CPU 通过流水线技术,让一条指令需要的时钟周期尽可能地少。
  • 指令数,代表执行我们的程序到底需要多少条指令、用哪些指令。这个很多时候就把挑战交给了编译器。同样的代码,编译成计算机指令时候,就有各种不同的表示方式。

三丶代码如何变成机器码

下面我们已一个java程序的运行为例

Java程序的运行过程确实涉及多个步骤,最终要将Java代码转换成机器码,以便计算机硬件能够执行。以下是详细的运行过程:

  1. 编写源代码
    开发者使用文本编辑器或集成开发环境(IDE)编写Java源代码,并将其保存为.java文件。
  2. 编译源代码
    使用Java编译器(比如javac命令)将.java文件编译成Java字节码。编译后生成对应的.class文件,每个.class文件对应源码中的一个类。
  3. 类加载
    当运行Java程序时,Java虚拟机(JVM)会通过类加载器(Class Loader)加载需要的.class文件。类加载器会检查这些文件,比如验证格式和检查安全性。
  4. 字节码验证
    字节码验证器(Bytecode Verifier)会检查字节码,确保它们符合Java语言规范,并且没有违反JVM的安全约束。
  5. 解释执行或即时编译
    JVM可以通过两种方式执行字节码:
    • 解释执行解释器(Interpreter)会逐条解释执行字节码,这种方式执行速度相对较慢
    • 即时编译(JIT编译)JIT编译器会将热点代码(执行频繁的字节码)编译成本地机器码以提高效率。这种编译是在运行时进行的,并直接在内存中完成
  6. 执行机器码
    一旦字节码被JIT编译器转换成机器码,CPU就能直接执行这些代码了。如果是解释执行,则JVM解释器会直接驱动CPU按字节码进行操作,而无需转换成机器码。

总结来说,Java程序从源代码到执行经历了编写、编译、加载、验证、执行等步骤。.java文件被编译成.class文件(字节码),然后被JVM加载并可能被JIT编译器转换成机器码执行。JVM为Java程序提供了一个与平台无关的运行环境,这也是Java可以"一次编写,到处运行"(Write Once, Run Anywhere)的原因。机器码是JVM执行Java字节码时,根据具体的操作系统和硬件实时生成的,这就是为什么Java程序需要安装对应平台的JVM来运行的原因。

在JVM内部,特别是在即时编译(Just-In-Time, JIT)阶段,实际上是存在将字节码转换成汇编代码的过程的,只不过这一过程对于开发者而言是透明的。

字节码 -> (JIT编译器)-> 汇编代码 -> (汇编器)-> 机器码

一般来说,我们用高级程序语言编译的代码,会经过编译转变为汇编代码,然后汇编器翻译为机器码的过程

java语言由于其一次编译,到处运行的特点,引入了jvm屏蔽操作系统的差异。

四丶CPU是如何执行指令的

一个 CPU 里面会有很多种不同功能的寄存器

  • PC 寄存器(Program Counter Register),我们也叫指令地址寄存器(Instruction Address Register)。顾名思义,它就是用来存放下一条需要执行的计算机指令的内存地址。
  • 指令寄存器(Instruction Register),用来存放当前正在执行的指令。
  • 条件码寄存器(Status Register),用里面的一个一个标记位(Flag),存放 CPU 进行算术或者逻辑计算的结果

CPU(中央处理单元)是计算机的核心组件,负责执行程序代码中的指令。它通过一系列的电子组件和逻辑电路来完成各种操作。以下是CPU运行指令和处理分支、跳转、循环等控制流指令的基本过程:

1.CPU如何运行指令:

  1. 取指令(Fetch):CPU从程序计数器(Program Counter, PC)中获取下一条要执行的指令的地址。然后从内存中将指令代码取出,放入指令寄存器。之后,程序计数器会更新,指向下一条指令的位置。

  2. 解码指令(Decode):指令解码器将取出的指令代码解析成CPU能理解的操作和操作数。

  3. 执行指令(Execute):执行单元根据解码后的指令进行相应的操作。这可能涉及到算术逻辑单元(ALU)进行算术或逻辑运算,访问寄存器获取数据,或者与内存交换数据等。

  4. 写回(Write-back):执行完毕后,结果被写回到CPU寄存器或者内存中。

这个过程被称为指令周期,是连续不断循环执行的。

2.处理分支、跳转和循环:

对于分支、跳转或循环这类控制流指令,CPU会根据条件来改变程序计数器的值,以此来改变执行流程。

  1. 分支指令(Branching):常用在条件语句中。CPU会评估条件是否成立,如果条件满足,程序计数器会被更新为分支目标地址,即跳转到程序中的另一部分继续执行。如果条件不满足,程序计数器则会指向顺序的下一条指令。

  2. 跳转指令(Jumping):用于无条件地改变程序的执行流程,程序计数器会被设置为跳转指令指定的目标地址。

  3. 循环指令(Looping):循环指令通常是通过比较和跳转指令的组合来实现的。CPU不断检查循环条件,根据条件是否满足来决定是继续执行循环体内的指令还是跳出循环。

3.现代CPU使用一系列技术来优化控制流指令的执行

  • 分支预测(Branch Prediction):CPU尝试预测分支指令的结果,以便提前取指令。如果预测正确,可以避免等待条件评估的时间,提高执行效率。
  • 指令流水线(Instruction Pipelining):在指令的不同执行阶段同时处理多条指令,以提高吞吐量。
  • 乱序执行(Out-of-Order Execution):CPU可能会改变代码中指令的执行顺序(如果没有数据依赖性),以减少等待时间并充分利用CPU资源。

五丶函数调用是如何实现的

1.函数调用,入栈出栈

函数编译之后,代码先执行了一条 push 指令和一条 mov 指令;在函数执行结束的时候,又执行了一条 pop 和一条 ret 指令。这四条指令的执行,其实压栈(Push)和出栈(Pop)操作

在真实的程序里,压栈的不只有函数调用完成后的返回地址。比如函数 A 在调用 B 的时候,需要传输一些参数数据,这些参数数据在寄存器不够用的时候也会被压入栈中。整个函数 A 所占用的所有内存空间,就是函数 A 的栈帧(Stack Frame)。

2.函数内联

以java为例,当然不只是java,任何语言都由函数

在Java中,函数内联(Method Inlining)是一个编译时优化技术,由Java虚拟机(JVM)的即时编译器(JIT Compiler)执行,而不是由Java源代码编译器(javac)执行。函数内联意味着将一个函数的代码直接插入到调用该函数的地方,这样做的目的是减少函数调用的开销,并有可能进一步优化由于上下文更明确的代码

函数调用涉及到一些开销,如保存调用现场、传递参数、跳转执行等,这些都会消耗时间和资源。通过内联,可以省去这些开销,因为调用函数的代码被替换成了函数体本身的代码

JIT编译器在运行时会基于代码的运行特征来做出是否内联的决策,通常倾向于内联以下类型的函数:

  1. 小函数:体积小的函数,如只包含几条指令的函数。

  2. 热点函数:被频繁调用的函数,这些函数的性能对整体应用性能有显著影响。

  3. 最终方法在Java中,被声明为final的方法无法被子类覆盖,因此调用这样的方法时,JIT编译器不需要解析动态分派,更容易做内联

  4. 私有方法:私有方法也无法被外部覆盖,因此也是内联的好候选。

  5. 静态方法:静态方法通常也不会被动态覆盖,因此更容易进行内联。

需要注意的是,JIT编译器也有内联失败的情况,比如当函数体太大时,或者函数内部包含复杂的控制流(如大量循环和分支)时。此外,如果函数调用涉及到动态绑定(如多态情况下的虚拟方法调用),JIT编译器可能无法确定哪个版本的方法会被调用,这时候也很难进行内联