架构学习-编程语言的演进

架构学习-编程语言的演进

Posted by liz on February 6, 2024

架构学习-编程语言的演进

前言

这里开始来尝试了解下架构层面的知识。

什么是冯·诺伊曼结构

冯·诺伊曼结构(英语:Von Neumann architecture),也称范·诺伊曼模型(Von Neumann model)或普林斯顿结构(Princeton architecture),是一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构。

早期的计算机是由各种电路组成的,这些电路通过组装出一个固定的电路板来执行一个特定的程序,一但需要修改程序功能,就需要重新组装电路板,所以早期的电脑是硬件化的,可编程的空间太小。

早期的计算机设计的时候,程序和数据是两个不同的概念。数据存放在存储器中,而程序作为控制器的一部分,这样的计算机效率低,灵活性比较差。冯·诺伊曼结构中,将程序和数据一样看待,将程序编码为数据,和数据一起存放在存储器中,这样计算机就可以通过调用存储器中的程序来处理数据了,这样意味着任何的程序只要转换成数据的形式存储在存储器中,要执行相应的程序只需要从存储器中依次取出指令,执行。这种设计使得硬件设计和软件设计可以分开执行,实现了可编程的计算机功能,大大促进了计算机的发展。

冯·诺伊曼结构的强大之处,它解决了一切可以通过计算解决的问题。

冯·诺伊曼结构中定义了5大组件:

  • 运算器;

  • 控制器;

  • 存储器;

  • 输入设备;

  • 输出设备;

architecture

中央处理器

其中运算器和控制器共同组成了计算机的中央处理器(Central Processing Unit),CPU 是一块超大规模集成电路,是计算机的运算核心和控制核心,CPU 的主要功能是解释计算机指令以及处理数据。

中央处理器负责程序(指令)的执行,指令存储在存储里面。计算机加电启动后,中央处理器从一个固定的存储地址开始执行。

中央处理器的指令大致有下面几种:

  • 计算类:进行各种计算,比如加减乘除、sin/cos 等等;

  • I/O 类:从存储读写数据,从输⼊输出设备读数据、写数据;

  • 指令跳转类:在满⾜特定条件下跳转到新的当前程序执⾏位置、调⽤⾃定义的函数。

存储器

存储器的主要功能是存储程序和各种数据,并且能在计算机运行过程中高速,自动的完成程序或者数据的存储。

从中央处理器的角度理解,存储可以简单分成两类:一类是内置支持的存储,通过常规的处理器指令可直接访问,比如寄存器,内存,计算机主板的 ROM。一类是外置存储,属于输入输出设备,中央处理器不能直接访问其中的数据。

冯·诺依曼体系中涉及的“存储”,指的是中央处理器内置⽀持的存储。

内存的存取速度会直接影响计算机的运行速度,由于 CPU 是高速器件,CPU 的运行速度远高于内存的读取速度的,为了解决 CPU 和内存速度不匹配的问题,在 CPU 和内存中直接设置了一种高速缓冲存储器 Cache。Cache 是计算机中的一个高速小容量存储器,其中存放的是 CPU 近期要执行的指令和数据,存取速度接近 CPU 的速度。

所以存储一般会分成两部分:内部存储器(内存)和外部存储器。

内部存储器:称为内存或者主存,是用来存放欲执行的程序和数据。需要有很高的存取速度。

外部存储器:主要用来存放”暂时“用不着的程序和数据,可以和内存交换数据。一般是磁盘、光盘、U盘、硬盘等。

输入输出设备

输入输出设备,大大扩展了计算机的能力,每个设备通过一个端口和中央处理器连接,通过这个端口中央处理器可以和设备进行数据交换。数据交换涉及到的数据格式由设备定义,中央处理器并不理解。

设备交换的发起方(设备使用方)来解释处理数据的含义。一般设备厂商或操作系统厂商,会提供设备的相关的驱动程序,把设备数据交换的细节隐藏起来,设备使用方只需要调用相关的接口函数就能操作设备了。

输入输出设备解决的根本问题是什么呢?

电脑的无限扩展能力,⽐如常⻅的外置存储如机械硬盘、光盘等,它们也是输⼊输出设备,但并不是⽤于交互,⽽是显著提升了电脑处理的数据体量。

同样的输入输出设备还可以是完全不同架构的电脑,比如 GPU 电脑、量⼦计算机。

汇编语言

在第一门面向程序员的变成语言出现之前,人们只能通过理解 CPU 指令的二进制表示,将程序以二进制数据方式刻录在存储上。

这个阶段的编程效率是及其低效的,软件和硬件的边界还非常模糊,并不存在软件工程师这样的职业,写程序并不是一个纯软件的行为,把程序刻录在存储上,往往还涉及到了硬件的电气操作。

为了解决效率的问题,汇编语言出现了,汇编语言将汇编语言编写的程序编译为 cpu 指令序列,并保存在外置的存储设备上。

汇编语言的出现,让写程序变成了一个传软件的行为,可以反复的修改程序,然后通过换边编译器翻译成机器语言,并写入到外置的存储设备。程序员就能按需执行该程序。

编程范式的进化

从汇编开始慢慢出现了很多的变成语言,同时也出现了很多的编程范式。

1、过程式

过程方式就是以一条条的命令的方式,让计算机按照我们的意愿来执行。

今天计算器的机器语言本身就是一条条指令构成,本身就是过程式的,所以过程式的语言最常见,每个语言都有一定过程式的思想,过程式的语言代表,C/C++,JavaScript,Go 等;

过程式编程中最核心的两个概念是结构体(自定义的类型)和过程(也叫函数)。通过结构体对数据进行组合,可以构建出任意复杂的自定义数据结构。通过过程可以抽象出任意复杂的自定义指令,复用以前的成果,简化意图的表达。

2、函数式

函数式编程中,函数是头等对象,一个函数既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或被分配给一个变量。

函数式编程更加强调,程序的执行结果而不是执行过程,倡导利用若干简单的执行单元,让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

函数式本质上是过程式编程的一种约束,它最核心的主张就是变量不可变,函数尽可能没有副作用。(对于通用语言来说,函数没有副作用是不可能的,内部有I/O行为函数的就有副作用)。

如果函数有副作用,我们将其称为过程。

什么是程序中的副作用?

在I/O模型中,我们希望在I到O中之间只有计算,如果计算的过程中,不仅出发了本次的I/O计算,还触发了和本次I/O不相关的任何事件,都称为副作用。

即变量不可变,函数就没有副作用,这样写代码出现错误的机会就少了,代码质量也能变得更高。函数方编程相对小众,使用函数式编程,代码质量高,但是门槛也会很高。

来个栗子:在过程式编程中,数组是一个最常规的数据结构,但是在函数式编程中因为变量不可变,对下标的数组元素修改,就需要复制整个数据(因为数组作为一个变量不可变),非常低效。

3、面向对象

面向对象在过程式的基础上,引入了对象(类)和对象方法(类成员函数),主张尽可能的把方法(过程)归纳到合适的对象(类)上,不主张全局函数(过程)。代表语言是 Java,C, C#, Go 等。

面向过程的程序设计是把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程继续把函数切分为子函数,把大块函数通过切割成小块函数来降低系统的复杂度。

面向对象的程序设计把计算程序设计成一组对象的集合,每个对象都可以接收其它对象发过来的信息,并处理这些信息,计算机程序的执行就是一些列消息在各个对象之间传递。

下面来个栗子来介绍下面向过程和面向对象的不同之处:

假定我们要处理学生的成绩,为了表示一个学生的成绩,使用面向过程可以这样写:

package main

import "fmt"

type Student struct {
Name  string
Score int
}

func PrintScore(std *Student) {
fmt.Println("学生", std.Name, "年纪", std.Score)
}

func main() {
std1 := Student{
Name:  "小明",
Score: 20,
}

PrintScore(&std1)
}

这是一种面向过程的写法,Student 结构体被用来存储学生的数据,PrintScore 用来打印学生的成绩,main 函数是程序的入口点,用于创建学生,打印每个学生的成绩成绩表,并打印学生的成绩。没有使用对象或方法,只是结构体和函数。

如果使用面向对象的设计思想,首先考虑的不是程序的执行流程,而是 Student 这种数据类型因该被当成一个对象,这个对象拥有 Name 和 Score 这两个属性。

如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后给对象发一个 PrintScore 消息,让对象自己把自己的数据打印出来。

package main

import "fmt"

type Student struct {
Name  string
Score int
}

func (s *Student) PrintScore() {
fmt.Println("学生", s.Name, "年纪", s.Score)
}

func NewStudent(name string, score int) *Student {
return &Student{
Name:  name,
Score: score,
}
}

func main() {
std := NewStudent("小明", 12)
std.PrintScore()
}

面向对象发消息时机上是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向对象的程序写出来就像这样:

bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.PrintScore()
lisa.PrintScore()

面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class 是一种抽象概念,比如我们定义的 Class——Student,是指学生这个概念,而实例(Instance)则是一个个具体的 Student,比如,Bart SimpsonLisa Simpson 是两个具体的 Student。

所以,面向对象的设计思想是抽象出 Class,根据 Class 创建 Instance。

面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。

参考

【冯·诺伊曼结构】https://zh.wikipedia.org/wiki/%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC%E7%BB%93%E6%9E%84 【面向对象编程】https://www.liaoxuefeng.com/wiki/1016959663602400/1017495723838528 【许式伟的架构课】https://time.geekbang.org/column/article/89668