现代CPU上的所需要的low level language特性_风闻
code2Real-有人就有江湖,有code就有bug2022-01-07 18:04
PDP-11深受软件设计师喜爱的原因,主要是在于其高度正规化指令集的设计,使得软件设计师可以容易地分别记住所有的运算码,以及指定运算子的方法。如此一来,给定运算子的方法(或称之为定址模式)便可以很容易地预测,这样子就不用去背一堆例外条件,或是特别受限的定址方式。
PDP-11所使用的指令集结构影响了C语言的语法。例如在C语言中,有着暂存器定址模式的增值与减值语法 ++i 与 i–。 如果 i 与 j 都是暂存器变数,那么 (–i) = (j++) 这样子的表示式就可以编译为单一机器码指令。由于对单精确与双精确浮点数没有不同的运算码,也造成C语言中缺乏单精确浮点数运算的运算模式。
基于PDP-11形成一个简单的CPU架构,大多数高级编程语言都是基于这个架构进行设计的。导致的结果是,现在大多数流行的语言都不能很好地抽象、利用近些年的硬件架构发展,尤其是vector processor、GPU 之类的消费级产品。
因为简单的东西容易普及,所以C语言设计时需要扼杀一些比较底层特性(low level feature):
1. 运行时指令修改。一个可以修改自己内存中指令的程序,在我们已经熟悉C语言的人眼里看起来,还是有点hack或者容易被识别成病毒,不过在过去这真的很方便做出来很小的程序以及另外一种范式的多态。
C语言显然在语言层面上没有对这种行为给予任何支持(我们只能获取函数代码的开始地址(函数指针)而不能获取结束地址……),32位以上操作系统也直接加把火:shell启动可执行文件或者加载动态库都会把指令部分内存页设置为只读属性了。有的CPU ICACHE也会使用一些较弱的一致性模型来省省(出现不匹配会有一个计数中断来做统计)。
2. 栈内存节省使用。
C语言在编译优化,定义参数传递ABI等场景,都在默认栈是无限大的,很没底线的使用栈内存(函数入口处按最坏情况分配栈内存而不是按需使用,函数参数压栈传递时也是按一种简单但经常产生浪费的规则来压栈)。其实这应该也说不上是C语言导致的,应该认为是体系结构内存模型和C语言互相放纵的结果。
ANSI C语言当年提供的这套只有内存和指令,平坦地址模式的虚拟机在那个时候已经很精简了(看看80年代日本的游戏机硬件复杂的开发过程吧……居然屏蔽和封装了远/近跳转,高速&低速内存 这些特定硬件提供出来的优化细节……),那个时候就已经认为他不是low level了。
中间几十年cpu基本都在沿着C语言的编程模型提升性能(加大频率&加大内存和cpu之间的cache)。如今cpu厂商在增加频率的大路上走不通了开始不断加一些新花样,对应于这些新的特色C语言的虚拟机再次被认为不够low level也是在情理之中的。
如果不受ANSI C的限制,采用厂家提供的固有功能,那么还是可以解决一些问题的:
1. Cache 控制问题。现在的C/C++编译器都有固有(intrinsic) 系统,对于x86 CPU,prefetch0, prefetch1, cflush, CPUID 一堆intrinsic 完全可以控制cache,只要你会用。(这世界上会用prefetch 的人不超过100个,其中50个是设计这些指令的人……)
2. Vector processor 问题。如果只说“low-level language”,各家硬件厂商的SIMD intrinsic 已经非常好用,各大编译器的支持让intrinsic 已经成了事实标准。
如果我们真的抛弃 C语言,我们需要的是什么语言?
1. 能显示操纵L1,L2,L3缓存
2. 可直接操纵100+寄存器
3. 程序员提供的分支预测启发算法
显然,我们需要一个新的 CPU 架构、新的指令集,和新的语言,这个工作太大了。
但是spir-v让我们看到希望,spir-v一方面支持low feature,一方面具有一定抽象能力:
1. 变量不可变。SPIR-V所有的局部变量都只支持单次赋值。
2. 支持Logical addressing模式。这种模式下,指针只是个抽象的概念,不能转成整数,也不能进行算术运算。
这两点可以看成是从LLVM IR分别删掉了赋值功能和内存地址功能。
卡马克早就说过:限制方法的灵活性几乎总会让你把事情做得更好。
所以,虽然SPIR-V主要是为GPU而设计的,但却比LLVM IR还要符合现代CPU上所需要的low level language特性。