我是一个不合理的人自豪的小事:我是向RISC-V ISA规范添加句子的近似原因。
带有X0的目的地的负载必须仍然介绍任何异常,并使任何其他副作用即使丢弃负载值也是如此。
在2016年夏天,我写了大部分初始RISC-V Go Compiler实现。 (Michael Pratt和Benjamin Barenblat在汇编程序,链接器和运行时工作,其他人跳入并最终完成了港口。)
我正在编写第一个版本的RISC-V SSA降低规则。这些规则将GO代码的通用,架构无关描述为RISC-V特定的操作集,最终降低到RISC-V指令中。
type t struct {a [5000]字节// we' ll解释出这个稍后b bool} func f(t * t){_ = t。 B}
F几乎没有。但不是什么。 f评估T.B以进行副作用。如果T是零,F ancics。
在Go编译器中,这是(不出所料)称为NIL检查。如果T为零,编译器将安排执行将发生故障的指令。
获取堆栈中T的值并将其放在轴寄存器中。
加载轴指向的值并用它做点什么。 AX周围的柱均衡AX寄存器中的指针。它在此处并不重要,这是测试的指示;它被选中,因为它是短暂的编码。它'这是重要的推进。如果负载故障,则运行时将接收信号并将其转换为恐慌。
典型的Go程序中有很多零检查。作为优化,运行时在地址0处分配保护页面,通常具有大小为4096字节。从地址且来自地址的任何加载4096将出现故障。
结果,如果您'重新取消引用具有小偏移量的结构字段,我们可以直接尝试从计算出的该结构字段的地址加载。如果指针为零,则计算出的地址将是< 4096,它' ll故障。在那里没有必要单独的,明确的零支票。
例如,如果我在上面使用的[20]字节,则* t.b需要从t加20加载。如果t为nil(0),则该地址为20,位于Guard页面中。
由于我们上面有一个[5000]字节字段,因此Guard页面' t足够,所以我们需要一个明确的零支票。
这使它听起来像明确的零检查非常罕见。他们'重新;他们也以其他方式出现。
RISC-V具有专用零寄存器X0。它始终保持零值,并丢弃它的写入。它' s喜欢/ dev / null和/ dev / zero滚动到一个。
这听起来像是一个nil检查的东西:我们可以将指针放在x0中的指针并加载到x0中。
它几乎与AMD64版本相同。第一个指令从堆栈加载指针。第二个指令将其解除到X0中。最终的指令返回。
如果你'重新加载一个值才能丢弃它,你真的需要加载它吗?如果你'重新写入x0,也许你可以跳过它。
来自AMD64的类似物。 CMOV指令有条件移动。如果设置了标志,则它会加载或移动值,而不是其他。它在编译这样的代码时出现:
Func g(x int)int {y:= 1如果x == 0 {y = 3}返回y}
TestQ设置EQ标志如果x为0.下一个指令在CX中放入AX和3中。最后,如果设置了EQ标志,我们将CX移动到AX中。 AX现在持有y的正确值返回。
如果CMOV指令包括来自存储器的负载,则即使将该值写入目标寄存器的位置是条件的,也会无条件地完成该负载。
我知道(并知道)关于硬件,我猜测为什么这是一个很好的决定。如果您'重新执行订单执行,您可能不知道旗帜将在达到CMOV指令时是什么。但内存负荷很慢。我们希望早期开始该内存负载以获得最大的好处。因此,能够无条件地执行负载是有用的,即使对编译器开发人员不方便。
但同样的考虑不适用于RISC-V。在那里没有关于指令是否写入X0的不确定性。跳绳将是简单又便宜的。
我们在规格中辩论了这个洞,但忽略了写下结论。
我们使用此定义的主要原因是用于触发副作用的内存映射I / O负载的清洁语义。相反的选择也是可靠的(它为您提供免费的非绑定预取指令)。
未兴望的灯光,未享受。但方便的Go' s nil checks。我已经问过确实有助于捆绑一个有点松散。