计算机组成原理实际上跟软件工程有点“亲”,但它的关系,就像两个邻居住同一个小区,一个负责修房子,一个负责管水电。 软件工程更偏向于如何花钱雇人干活,如何让流程跑得通,如何保证代码能稳定运行。它关切的是:要把一个大型产品拆成一个个小的模块,每个模块像个小模块,要么能独立工作,要么能互相配合,看着像个前台能独立处理业务。它厌恶那些能直接运行但结构烂的“黑作坊”代码。 而计算机组成原理,是从底层去算这笔账。它关心的是 CPU 是如何升级的,指令流水线如何设计的,寄存器里到底有多少数据。它不关心那个软件系统好不好,只关心这块硬件能不能在 1 秒内把计算任务做完。

这两者有时候说不通,当你把底层代码写成上层应用时,你会发现底层那堆复杂的指令调度,变成上层代码后根本就消亡了,但这确实挺悬的。 举个例子,假设你要写个视频编辑软件。按软件工程来说,你得先切分代码,把导入、剪辑、导出这三个核心功能拆成独立的模块。每个模块都要有详细的接口文档,不然到时候两个模块打架,哪位也不服哪位。

这时候你可能得花一周工夫梳理接口,就连为了兼容性要改三次框架。 但到了计算机组成原理层面,你就要启动琢磨如何把视频数据塞进那个内存条,如何让 CPU 的流水线把帧帧画面飞快地流转起来。

这时候难题就变了,你可能得去调整缓存的地址映射逻辑,得去研究总线如何带宽够不够。

要是你单纯只盯着软件架构,可能会把那个最关键的流水线设计给忽略了,害得视频导出时卡顿,别看软件框架写得挺完美,但实际跑起来就是慢成一包。

这种“软件好看,硬件卡死”的情况,确实存有。 目前讲点具体的数据,为了说明硬件如何影响软件的表现。我们能够看看一个典型的高性能渲染机。

这机器配备了多个核心,每个核心都有自己专属的缓存,核心数量大约是 32 个,缓存大小是 8MB,每次能加载的数据量在 500KB 左右,至于带宽,那是 500GB 每秒。

这些数字拍板了软件能跑多快的线程,能处理多高分辨率的画面。 要是把这个数据放到软件里,你会发现,线程调度器的设计彻底由这些硬件参数拍板。软件不可能凭空出现一个完美的调度器,它务必依赖硬件分配的资源。

要是硬件的延迟忒高,软件里的工夫片就无法按时到期。

这就好比你去办一件事,你给的期限是 5 分钟,但实际处理这块需求 10 分钟,你只能被迫把任务拆分,要么硬塞进队列,要么干脆把任务取消。 还有一种情况,就是并行计算。大量现代软件需求与此同时处理数百个任务。

这不能靠工夫片轮转,得靠并行计算

这时候就需求 CPU 赞成多核,赞成向量宽,赞成 SIMD 指令集。软件工程师要写一个应用,要确保它能在 10 个核上与此同时跑,与此同时把数据分装好,再丢进内存。

要是硬件不赞成这种细粒度的缓存标量换,软件就得去重写,要么干脆拉倒这个应用。 重复计算也是个经典场景。

比如你在写一个图像处理库,可能同一个滤镜要调用几千次。软件层面能够优化,让函数只执行一次,调用时直接复用结局。但要是底层硬件的缓存命中率低,CPU 每次都要去访问内存,每次访问都要排队等待,软件层的复用就白做了。

这时候硬件的延迟和带宽就成了瓶颈。 还有那个著名的冯·诺依曼瓶颈。大量工程师都吐槽过,CPU 的性能别看逆天,但数据读写忒慢,成了 biggest bottleneck。软件挺难突破这个物理极限,要不就软件能直接利用物理内存操作,要么想办法绕过那个瓶颈。

比如某些编译器能够把代码编译成汇编,就连把二进制直接转成硬件指令,这时候“软件”就成了“硬件”的底层实现。 有时候我们会认定软件是独立的,但事实并非如此。软件是硬件在特定的时序、特定的资源约束下的产物。一个完美的软件架构,需求一个完美的硬件底座。

反之,一个出色的底层硬件,能不能跑出一个出色的软件系统,那又是另一回事了。 故此,计算机组成原理软件工程,不是一个教条,而是一个动态的匹配过程。它们共同成长,互相牵制。

要是你只懂软件,硬件给你送不来数据;要是你只懂硬件,软件给你搭个台子,过不去。最好的状态,就是让两者的参数值在一个合理的范围内,既能让硬件跑得飞快,又能让软件活得体面。

毕竟,最终用户不是要一堆完美的代码,而是要一个能用的东西。