The Go team is working to implement a minimum viable register-based calling convention in Go 1.16. This change could provide 5-10% throughput improvement, according to the Google Go team, while keeping backward compatibility for code using Go current stack-based calling convention.
Go use of a stack-based calling convention may be traced back to its roots in Plan 9, specifically its commitment to Plan 9 ABI. By switching to a register-based calling convention, Go is now aiming to remove one drawback of its original design that sets it apart from most modern language implementations on most platforms, the Go team implies.
Stack-based calling convention relies on passing arguments and results through the stack. While this approach greatly simplifies implementation, it exacts a significant cost in terms of performance. Indeed, based on their own benchmarks, Go developers estimate register access is roughly 40% faster than stack access. Additionally, memory traffic generated by function calls has also a negative effect on performance.
One of the key concerns in designing Go's new register-based calling convention is making it well-behaved with regard to Go coroutines.
One reason goroutines scale so well is that the Go runtime dynamically resizes their stacks, but this imposes requirements on the ABI that aren’t satisfied by non-Go functions, thus requiring the runtime to transition out of the dynamic stack regime on a foreign call. Another reason is that goroutines are scheduled by the Go runtime rather than the OS kernel, but this means that transitions to and from non-Go code must be communicated to the Go scheduler.
This hints to the necessity for Go calling convention to use its own ABI, rather than relying on each supported platform ABIs. Adopting platform ABIs would also have additional cons, such as their vast and subtle heterogeneity which would increase implementation complexity, and the fact that many ABIs were derived from C's ABI, which is not a particularly good fit for Go.
The current proposal aims thus to define a common ABI to be used across all platforms, with each platform specifying a sequence of integer and floating point registers and using a shared set of rules as much as possible. Furthermore, the calling convention should support many-word return values, which are pretty common in Go code. The calling convention should also enable a first-class call frame representation which is required by the go
and defer
statements as well as by reflection.
Those and other requirements that are actually considered by the Go team will require a number of changes in Go toolchain, including its compiler, linker, and runtime. For example, the compiler will be required to support abstract argument registers, late call lowering, ABI bridges, and several additional new features. The runtime will be in charge of ensuring first-class call frame representation, growing the stack when additional stack space is required, etc.
While this change is meant to be Go-1 compatible, there are a couple of cases where existing code will break, such as assembly code that invokes Go closures, and code performing unsafe.Pointer
arithmetic on pointers to arguments in order to observe the contents of the stack.
As mentioned, Go 1.16 will only include a minimum viable implementation of the new calling convention for the amd64 platform. To get the full list of requirements and detailed design, do not miss the official proposal.