Motivation
我们知道体系结构或者操作系统接口一般都会约定调用者保存和被调用者保存的寄存器。这两天在看RISCV源码的时候发现了一个有意思的commit,主要是实现了原本没有的一个优化,就是在进行函数调用的时候,被调用者和调用者都只保存那些当前函数上下文中正在用到的寄存器,可以忽略掉当前函数体中根本没有被分配的寄存器。
被调用者部分主要是通过在TargetRegisterInfo::getCalleeSavedRegs()以及TargetFrameLowering::determineCalleeSaves()中设置相应的判断条件来实现的。前者返回体系结构定义中定义的被调用者保存寄存器,后者则是在生成函数入口的时候,根据前者返回的信息,以及自己的实际寄存器使用情况,来决定在入口处保存哪些寄存器。
Detail Analyze
这里的实现中,需要对中断函数处理函数,即标记为带有interrupt属性的函数做一个特殊处理:如果在这种函数的函数体中出现了对其他函数的调用,那么中断处理函数的入口处就要无条件地保存并恢复所有的寄存器。这是因为中断处理函数是收到外部中断的时候,突然被打断了原本的执行流程,并进入了中断上下文。这个过程本质上不是一个函数调用,它没有给被中断的函数留出保存自己应该保护的寄存器的时间,而是直接就修改了PC调到了中断处理函数的起始地址。
在这种情况下,如果中断处理函数中没有去调用其他什么东西,那么编译器就知道整个函数体执行的过程中都用到了哪些寄存器,可以只在入口处保存好这些寄存器,离开中断上下文的时候恢复它们就可以了。但如果中断处理函数中调用了其他的什么函数,此时被调用的函数在入口处只负责保存好那些规定为被调用者需要保护的寄存器,但却会对调用者负责保护的寄存器任意使用而不加保存。由于中断处理函数也并不知道被调用的函数会使用哪些被中断的函数当时正在使用却没有来得及保存的寄存器,所以为了保险起见,它就必须在入口处把所有调用者本应该保存的寄存器也保存好,相当于要保存全部寄存器。
本质上,这种特殊处理是因为被中断的函数还没有来得及保存好自己应该保存的寄存器,所以这部分工作就只能交给中断处理函数来负责了。但是全套的调用者保存寄存器其实是很多的,有int, fp32, fp64三组,全保存下来会是巨大的开销。所以看起来中断上下文最好不要真的发起函数调用,能尽量inline最好。
LLVM Note
获取一个MachineFunction的各种属性信息:
MachineFunction &MF;
MachineFrameInfo &MFI = MF.getFrameInfo();
MF.getFunction().hasFnAttribute("interrupt");
MFI.hasCalls();
检查一个MachineFunction是否生成了栈帧(Frame Pointer):
bool RISCVFrameLowering::hasFP(const MachineFunction &MF) const {
const TargetRegisterInfo *RegInfo = MF.getSubtarget().getRegisterInfo();
const MachineFrameInfo &MFI = MF.getFrameInfo();
return MF.getTarget().Options.DisableFramePointerElim(MF) ||
RegInfo->needsStackRealignment(MF) || MFI.hasVarSizedObjects() ||
MFI.isFrameAddressTaken();
}