[LLVM Study Notes] Context Switching and Calling Conventions

Motivation

We know that an architecture or an OS ABI typically specifies caller-saved and callee-saved registers. While reading the RISCV source over the past couple of days, I came across an interesting commit. It mainly implements an optimization that did not previously exist: during a function call, both the callee and the caller save only the registers that are actually used in the current function context, and can ignore registers that are never allocated within the current function body.

On the callee side, this is mainly implemented by adding the appropriate conditions in TargetRegisterInfo::getCalleeSavedRegs() and TargetFrameLowering::determineCalleeSaves(). The former returns the callee-saved registers defined by the architecture, while the latter, when generating the function prologue, decides which registers to save at entry based on the information returned by the former and the function's actual register usage.

Detail Analyze

In this implementation, interrupt handlers—functions marked with the interrupt attribute—require special handling: if such a function contains a call to another function, then at the interrupt handler's entry it must unconditionally save and restore all registers. This is because an interrupt handler is entered when an external interrupt suddenly breaks the original execution flow and switches into the interrupt context. This process is not a function call in essence: it does not give the interrupted function time to save the registers it should protect; instead, it directly changes the PC and jumps to the start address of the interrupt handler.

In this case, if the interrupt handler does not call anything else, the compiler knows which registers are used throughout the function body, so it can save only those registers at entry and restore them when leaving the interrupt context. But if the interrupt handler calls some other function, then the callee will only save, in its prologue, the registers that are defined as callee-saved, and will freely use caller-saved registers without saving them. Since the interrupt handler also has no way of knowing which registers the callee might use among the ones that the interrupted function was using at the time but had no chance to save, to be safe it must also save, at entry, all registers that the caller would have been responsible for saving—effectively, it has to save all registers.

Essentially, this special handling exists because the interrupted function did not have time to save the registers it should have saved, so that work can only be done by the interrupt handler. However, the full set of caller-saved registers is actually quite large, with three groups: int, fp32, and fp64. Saving them all incurs a huge overhead. So it seems best for the interrupt context to avoid making function calls; using inline as much as possible is preferable.

LLVM Note

Get various attribute information from a MachineFunction:

MachineFunction &MF;
MachineFrameInfo &MFI = MF.getFrameInfo();
MF.getFunction().hasFnAttribute("interrupt");
MFI.hasCalls();

Check whether a MachineFunction generates a stack frame (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();
}
comments powered by Disqus
Published:
2018-08-04
Category:
Tag: