FAQ第二期|世界上第一个编程语言是怎么来的?
文章目录
你好,我是徐文浩,今天是第二期 FAQ,我搜集了第 3 讲到第 6 讲,大家在留言区问的比较多的问题,来做一次集中解答。
有些问题,可能你已经知道了答案,不妨看看和我的理解是否一样;如果这些问题刚好你也有,那可要认真看啦!
希望今天的你,也同样有收获!
Q1:为什么 user + sys 运行出来会比 real time 多呢?
我们知道,实际的计算机运行的过程中,CPU 会在多个不同的进程里面切换,分配不同的时间片去执行任务。所以,运行一个程序,在现实中走过的时间,并不是实际 CPU 运行这个程序所花费的时间。前者在现实中走过的时间,我们叫作 real time。有时候叫作 wall clock time,也就是墙上挂着的钟走过的时间。
而实际 CPU 上所花费的时间,又可以分成在操作系统的系统调用里面花的 sys time 和用户态的程序所花的 user time。如果我们只有一个 CPU 的话,那 real time >= sys time + user time。所以,我当时在文章里给大家看了对应的示例。
不过,有不少同学运行出来的结果不是这样的。这是因为现在大家都已经用上多核的 CPU 了。也就是同一时间,有两个 CPU 可以同时运行任务。
你在一台多核或者多 CPU 的机器上运行,seq 和 wc 命令会分配到两个 CPU 上。虽然 seq 和 wc 这两个命令都是单线程运行的,但是这两个命令在多核 CPU 运行的情况下,会分别分配到两个不同的 CPU。
于是,user 和 sys 的时间是两个 CPU 上运行的时间之和,这就可能超过 real 的时间。而 real 只是现实时钟里走过的时间,极端情况下 user+sys 可以到达 real 的两倍。
你可以运行下面这个命令,快速验证。让这个命令多跑一会儿,并且在后台运行。
|
|
然后,我们利用 top 命令,查看不同进程的 CPU 占用情况。你会在 top 的前几行里看到,seq 和 wc 的 CPU 占用都接近 100,实际上,它们各被分配到了一个不同的 CPU 执行。
我写这篇文章的时候,测试时只开了一个 1u 的最小的虚拟机,只有一个 CPU,所以不会遇到这个问题。
Q2:时钟周期时间和指令执行耗时有直接关系吗?
这个问题提的得非常好,@易儿易 同学的学习和思考都很仔细、深入。
“晶振时间与 CPU 执行固定指令耗时成正比”,这个说法更准确一点。我们为了理解,可以暂且认为,是晶振在触发一条一条电路变化指令。这就好比你拨算盘的节奏一样。算盘拨得快,珠算就算得快。结果就是,一条简单的指令需要的时间就和一个时钟周期一样。
当然,实际上,这个问题要比这样一句话复杂很多。你可以仔细去读一读专栏关于 CPU 的章节呢。
从最简单的单指令周期 CPU 来说,其实时钟周期应该是放下最复杂的一条指令的时间长度。但是,我们现在实际用的都没有单指令周期 CPU 了,而是采用了流水线技术。采用了流水线技术之后,单个时钟周期里面,能够执行的就不是一个指令了。我们会把一条机器指令,拆分成很多个小步骤。不同的指令的步骤数量可能还不一样。不同的步骤的执行时间,也不一样。所以,一个时钟周期里面,能够放下的是最耗时间的某一个指令步骤。
这样的话,单看一条指令,其实一定需要很多个时钟周期。也就是说,从响应时间的角度来看,一个时钟周期一定是不够执行一条指令的。但是呢,因为有流水线,我们同时又会去执行很多个指令的不同步骤。再加上后面讲的像超线程技术等等,从吞吐量的角度来看,我们又能够做到,平均一个时钟周期里面,完成指令数可以超过 1。
想要准确理解 CPU 的性能问题,请你一定去仔细读一读专栏的整个 CPU 的部分啊。
Q3:为什么低压主频只有标压的 2/3?计算向量点积的时候,怎么提高性能?
低压和低主频都是为了减少能耗。比如 Surface Go 的电池很小,机器的尺寸也很小。如果用上高主频,性能更好了,但是耗电并没有下来。
另外,低电压对于 CPU 的工艺有更高的要求,因为太低的电压可能导致电路都不能导通,要高主频一样对工艺有更高的要求。所以一般低压 CPU 都是通过和低主频配合,用在对于移动性和续航要求比较高的机器上。
向量计算是可以通过让加法也并行来优化的,不过真实的 CPU 里面其实是通过 SIMD 指令来优化向量计算的,我在后面也会讲到 SIMD 指令。
Q4:世界上第一个编程语言是怎么来的?
如果你去计算机历史博物馆看一下真机,就会明白,第一台通用计算机 ENIAC,它的各种输入都是一些旋钮,可以认为是类似用机器码在编程,后来才有了汇编语言、C 语言这样越来越高级的语言。
编程语言是自举的,指的是说,我们能用自己写出来的程序编译自己。但是自举,并不要求这门语言的第一个编译器就是用自己写的。
比如,这里说到的 Go,先是有了 Go 语言,我们通过 C++ 写了编译器 A。然后呢,我们就可以用这个编译器 A,来编译 Go 语言的程序。接着,我们再用 Go 语言写一个编译器程序 B,然后用 A 去编译 B,就得到了 Go 语言写好的编译器的可执行文件了。
这个之后,我们就可以一直用 B 来编译未来的 Go 语言程序,这也就实现了所谓的自举了。所以,即使是自举,也通常是先有了别的语言写好的编译器,然后再用自己来写自己语言的编译器。
更详细的关于鸡蛋问题,可以直接看 Wikipedia 上这个链接,里面讲了多种这个问题的解决方案。
Q5:不同指令集中,汇编语言和机器码的关系怎么对应的?
不同指令集里,对应的汇编代码会对应这个指令集的机器码呀。大家不要把“汇编语言”当成是像 C 一样的一门统一编程语言。
“汇编语言”其实可以理解成“机器码”的一种别名或者书写方式,不同的指令集和体系结构的机器会有不同的“机器码”。
高级语言在转换成为机器码的时候,是通过编译器进行的,需要编译器指定编译成哪种汇编 / 机器码。
物理机自己执行的时候只有机器码,并不认识汇编代码。
编译器如果支持编译成不同的体系结构的汇编 / 机器码,就要维护很多不同的对应关系表,但是这个表并不会太大。以最复杂的 Intel X86 的指令集为例,也只有 2000 条不同的指令而已。
Q6:某篇文章大段大段读不懂怎么办?
@胖胖胖 同学说得很好。在专栏最开始几篇,或者到后面比较深入的文章,很多非科班的或者基础不太好的同学,会觉得读不下去,甚至很多地方看不懂。这些其实都是正常现象。
即便我在写的时候,已经尽可能考虑得比较完善,照顾大家的情况,但是肯定无法面面俱到。在我平时学习过程遇到拦路虎的时候,我一般有两种方法,这里跟你分享一下。
第一种,硬读。
你可能说了,这也叫方法吗?没错,事实就是这样。如果这个知识点,我必须要攻克,就想要搞明白,那我就会尽我所能,去看每一个字眼,把每个不理解的地方,都一点一点搞明白。不吝啬花费时间和精力。
当然这种情况适合我对这个内容完全不了解,或者已经基本了解,现在需要进一步提升的情况下。因为,在完全不了解一个知识的时候,这个壁垒是很高的。如果不想办法突破的话,那可能就没办法了解这个新的领域。而在已经基本了解某个领域或者某块知识的情况下,我去攻克一些更高难度的知识,很多时候也需要同样的方法,我会建立在兴趣的基础上去硬读,但是之后会非常非常有成就感。
第二种,先抓主要矛盾,再抓细节问题。
很多时候,大家在对一个知识不了解的时候,会感觉很“恐慌”。其实完全没必要,大家学任何东西都是从不会到会这么一个过程。就像@胖胖胖 同学说的那样,先找出这篇文章的主干,先对这些东西有个大致的概念。如果有需要,在之后的过程中,你还会碰到,你可以再重读,加深印象。
有时候,学习知识可以尝试“短期多次”。也就是说,看完一遍之后,如果不明白,先放下,过一段时间再看一遍,如果还不明白,再过一段时间再看。这样循环几次,在大脑中发酵几次,说不定就明白了,要给大脑一个缓冲的时间。
好了,今天的答疑到这里就结束了。不知道能否帮你解决了一些疑惑和问题呢?
我会持续不断地回复留言,并把比较好的问题精选出来,作为答疑。欢迎你继续在留言区留言,和大家一起交流学习。
文章作者 anonymous
上次更新 2024-03-11