你好,我是徐文浩,今天是第二期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的两倍。

你可以运行下面这个命令,快速验证。让这个命令多跑一会儿,并且在后台运行。

time seq 100000000 | wc -l &

然后,我们利用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:某篇文章大段大段读不懂怎么办?

@胖胖胖 同学说得很好。在专栏最开始几篇,或者到后面比较深入的文章,很多非科班的或者基础不太好的同学,会觉得读不下去,甚至很多地方看不懂。这些其实都是正常现象。

即便我在写的时候,已经尽可能考虑得比较完善,照顾大家的情况,但是肯定无法面面俱到。在我平时学习过程遇到拦路虎的时候,我一般有两种方法,这里跟你分享一下。

第一种,硬读。

你可能说了,这也叫方法吗?没错,事实就是这样。如果这个知识点,我必须要攻克,就想要搞明白,那我就会尽我所能,去看每一个字眼,把每个不理解的地方,都一点一点搞明白。不吝啬花费时间和精力。

当然这种情况适合我对这个内容完全不了解,或者已经基本了解,现在需要进一步提升的情况下。因为,在完全不了解一个知识的时候,这个壁垒是很高的。如果不想办法突破的话,那可能就没办法了解这个新的领域。而在已经基本了解某个领域或者某块知识的情况下,我去攻克一些更高难度的知识,很多时候也需要同样的方法,我会建立在兴趣的基础上去硬读,但是之后会非常非常有成就感。

第二种,先抓主要矛盾,再抓细节问题。

很多时候,大家在对一个知识不了解的时候,会感觉很“恐慌”。其实完全没必要,大家学任何东西都是从不会到会这么一个过程。就像@胖胖胖 同学说的那样,先找出这篇文章的主干,先对这些东西有个大致的概念。如果有需要,在之后的过程中,你还会碰到,你可以再重读,加深印象。

有时候,学习知识可以尝试“短期多次”。也就是说,看完一遍之后,如果不明白,先放下,过一段时间再看一遍,如果还不明白,再过一段时间再看。这样循环几次,在大脑中发酵几次,说不定就明白了,要给大脑一个缓冲的时间。


好了,今天的答疑到这里就结束了。不知道能否帮你解决了一些疑惑和问题呢?

我会持续不断地回复留言,并把比较好的问题精选出来,作为答疑。欢迎你继续在留言区留言,和大家一起交流学习。

评论