Skip to content

cmd/compile: switch to a register-based calling convention for Go functions #40724

Open

Description

@aclements

I propose that we switch the Go internal ABI (used between Go functions) from stack-based to register-based argument and result passing for Go 1.16 1.17.

I lay out the details of our proposal and the work required in this document.

The ABI specification can be found here.

This was previously proposed in #18597, where @dr2chase did some excellent prototyping work that our new proposal builds on. With multiple ABI support, we’re now in a much better position to execute on this without breaking compatibility with the existing body of Go assembly code. I’ve opened this new issue to focus on discussion of our new proposal.

/cc @dr2chase @danscales @thanm @cherrymui @mknyszek @prattmic @randall77

An incomplete and evolving list of tasks:

  • Define ABI specification (CL, @aclements)
  • Add GOEXPERIMENT=regabi and asm macro (CL)
  • cmd/compile: late call lowering (CLs)
  • cmd/compile: implement register arguments (@dr2chase)
    • spill around morestack call (CL)
    • add temporary ABI pragma for testing (CL)
    • add abstract argument registers (WIP CL)
    • implement ABI rules (CL)
    • use abstract argument registers
  • cmd/compile: implement register returns
  • cmd/compile: ABI wrappers (CL, old CL)
    • cmd/link: mangle wrapper names for ELF (CL)
    • cmd/link: mangle wrapper names for PE (CL)
    • implement zeroing of X15 in ABI0->ABIInternal wrappers (CL)
  • cmd/asm: add syntax for runtime packages to reference ABIInternal symbols (CL)
    • Mark runtime.{goexit,asyncPreempt}, reflect.{makeFuncStub,methodValueCall} definitions ABIInternal (CL)
    • Mark reference to runtime.callbackWrap in sys_windows_amd64.s as ABIInternal (CL)
  • runtime: fix assembly closure calls (mcall) (@aclements, CL)
  • reflect: support calling ABIInternal (CL)
  • reflect: support MakeFunc and method value calls (CL)
  • go and defer statements
    • cmd/compile: desugar go/defer to zero-argument (@thanm, CL)
    • cmd/compile: fix defer recover() (@cherrymui, CL)
    • cmd/compile: desugar go/defer with results (@cherrymui, CL)
  • cmd/cgo: decouple cgo callbacks from ABI (CL)
  • runtime: finalizer support (CL)
  • runtime: Windows callback support (CL)
  • cmd/internal/obj: introduce and use RegEntryTmp registers in prologue (@mknyszek, CL)
  • cmd/compile: generate correct ABI0 argument maps for body-less functions (see WriteFuncMap) (@dr2chase, CL)
  • cmd/compile: make cgo_unsafe_args generate an ABI0 function (or an ABIInternal-with-no-registers function) (@cherrymui, CL)
  • runtime: update debug call protocol (CL)
  • Fix ABI0/ABIInternal confusion in cgo (CL)
  • Run signature fuzzer and fix bugs (everyone)
  • Get all.bash to pass on Linux, Windows, and Darwin (everyone)

High-priority non-critical path

  • Export ABI information across package boundaries to eliminate more wrappers (@aclements, CL)
  • Make funcPC always return the "native" PC of a function (maybe also introduce ABIOther) (@cherrymui, CL)
  • Strip unnecessary ABIInternal annotations once funcPC is in
  • Tracebacks
    • Define extended argument metadata format (in progress, @cherrymui)
    • cmd/compile: produce extended argument metadata (@cherrymui, CL)
    • runtime: consume extended argument metadata (@cherrymui, CL)
    • cmd/compile,runtime: traceback metadata for spill slot liveness
  • DWARF
    • cmd/compile: ensure arguments have appropriate DWARF locations
    • Ensure GDB and Delve can find register arguments
    • [ ] cmd/compile: add a DWARF vendor attribute with function argument frame size (context)
  • Wrapper mangling for non-critical arches
    • cmd/link: mangle wrapper names for Mach-O (CL)
    • cmd/link: mangle wrapper names for xcoff
    • cmd/link: mangle wrapper names for Plan 9
  • runtime: simplify go/defer call paths
    • assert that go/defer frames are zero sized (CL)
    • replace defer reflectcall with a direct closure call (CL)
  • cmd/internal/obj: reject splittable ABIInternal functions without morestack spill info (e.g., asm functions) because we can't generate a correct morestack path (CL)
  • cmd/asm: don't reference args_stackmap in ABIInternal functions (because it's for ABI0) (CL)
  • Port performance-critical assembly to ABIInternal (see CL 296372 for valuable functions) (CL 308931, CL 310184)
  • math: fix overhead of assembly->Go calls CL 310331
  • Release notes
    • Mention undocumented behavior we've observed applications depending on that may have changed.
      • Using reflect.ValueOf(fn).Pointer() to get the PC of an assembly function will now return the PC of the ABI wrapper
    • Performance surprise: calling from assembly to Go is slightly more expensive.
    • Performance surprise: calling an assembly function via a closure is slightly more expensive.
    • Traceback format is much improved

Enabling steps

  • Enable regabiwrappers by default on amd64 (Linux, Windows, and Darwin)
  • Enable regabidefer
  • Enable regabireflect
  • Enable regabig
  • Enable regabiargs

Testing

  • Function signature fuzzer (ongoing)
    • Static call, closure call, method call, interface call, reflect.{ValueOf(target),MakeFunc(x, target),Method(x)}.{Call,Interface}, called from defer, called from go, called as a finalizer
    • {Big,Small} {argument,result} in {memory,registers} and {not addressed,addressed and not leaked to heap,addressed and leaked to heap}
    • Use defer in a test function (check arguments, modify results)
    • Pass pointers-to-stack and pointers-to-heap
    • Cause result to move to heap
    • runtime: add MoveStackOnNextCall, assertions for pointer-to-stack/pointer-to-heap, assertions for live/dead (@aclements, CL)
    • Integrate runtime testing hook
    • Clean up fuzzer, and get into tools repo.
  • cmd/compile: revive clobberdead (CL)
  • cmd/compile: add clobberdeadreg (CL)

Post-MVP

  • Move g from TLS to register (CL)
    • Use g register on non-Linux OSes (see CL)
  • Reserve and use X15 as zero register (CL)
    • [ ] Should this be X0 for more compact instruction encoding?
  • Port to other platforms
    • amd64
    • arm64
    • mips
    • ppc
    • s390x
    • risc-v
    • [ ] porting guide
  • Eliminate legacy ABI paths for all platforms, even if they're using ABIInternal with 0 registers
  • Eliminate spill slots?
  • Pass pointer-shaped method receiver in context register (cmd/compile: pass pointer-shaped receivers using context register #44827)
  • runtime: simplify go/defer call paths
    • simplify go/defer paths to assume zero args/results
    • simplify special _defer allocator and remove special _defer cases from mallocgc (because _defer records will be fixed size).
  • cmd/vet: add syntax and checks for register arguments and results in assembly like x+0(FP)
  • cmd/vet: check argument size for ABIInternal functions and fix runtime asm declarations

Cleanup (can be done later)

  • runtime: port all assembly -> Go calls to ABIInternal?
  • Eliminate ABIAlias support from cmd/compile, cmd/link, and object format
  • remove now-redundant fields from ssa.AuxCall
  • rationalize use of LocalsOffset/FrameOffset (for registers that use a link register, e.g. arm64, powerppc).
  • cmd/compile: revisit abiutils.go and clean up the API (also reduce ABIConfig copying)
  • cmd/internal/obj: generate stack maps in obj so we can better compact the morestack/body maps and reduce subtlety?
  • cmd/compile,runtime: re-enable passing arguments to go in runtime (context)
  • cmd/compile: always attach ir.Func to function ir.Names (context)
  • cmd/compile: support ABIInternal for tail calls (context)
  • cmd/compile: once wrapping of defers is on for all arch/OS combinations, it should be possible to simplify or remove some of the openDefer processing in ssagen (notablly we could get rid of openDeferSave()
  • [ ] runtime: think about an ABI-insensitive debugCall Delve uses DWARF information
  • Fix up named slots & value association
  • fix firstpos in ssa.go – sometimes it's late.

Activity

  1. added this to the Go1.16 milestone on Aug 12, 2020
  2. seebs commented on Aug 12, 2020

    @seebs
    Contributor

    Once upon a time, I was one of several people whose primary work activity for I think two days was tracking down a weird kernel crash, which was ultimately caused by callee-saved registers. Well, the crash was caused by a [FOO_MAX] array being overrun. But the thing where the result of that overrun was to overwrite a single automatic variable, which never had its address taken, five call frames away? That was caused by callee-saved registers.

    Which is to say:

    Platform ABIs typically define callee-save registers, which place substantial additional requirements on a garbage collector. There are alternatives to callee-save registers that share many of their benefits, while being much better suited to Go.

    +1 +1 +1 +1 +1 +1 [...]

  3. ncw commented on Aug 13, 2020

    @ncw
    Contributor

    If I'm understanding the proposal correctly, assembler code will continue to be written with the current stack based calling conventions.

    I understand the excellent reasoning behind doing this, I'll just note that this is surely going to frustrate assembly code writers not being able to pass and receive args in registers as often assembly code fragments are tiny and the overhead of calling them is massive.

  4. mknyszek commented on Aug 13, 2020

    @mknyszek
    Contributor

    @ncw This restriction to ABI0 for assembly writers isn't necessarily permanent. See https://go.googlesource.com/proposal/+/master/design/27539-internal-abi.md#proposal.

  5. laboger commented on Aug 13, 2020

    @laboger
    Contributor

    Right now writeBarrier is a special builtin that allows arguments to be passed in registers. Couldn't that same concept be extended to others like memmove and memcpy to avoid call overhead even though they are asm?

  6. aclements commented on Aug 13, 2020

    @aclements
    MemberAuthor

    @laboger , yes, we're considering special-casing just a few assembly functions. The hottest ones by far are memmove, memclrNoHeapPointers, and possibly syscall.Syscall. The write barrier calling convention is very specialized because its context is so unusual and performance-sensitive. We'll probably just switch the others to use the register ABI rather than anything more specialized (and teach the toolchain that those are special even though they're assembly).

  7. mknyszek commented on Aug 14, 2020

    @mknyszek
    Contributor

    A comment on the "stack growth" section of the doc, specifically about spilling register state for morestack into the guard space for a goroutine (copied from Gerrit):

    I worry a little bit about cases where functions marked nosplit eventually call into a non-nosplit function. If the nosplit frames are large (but don't exceed the guard space) we might find out that we don't have any space left. What should we do in this case? Throwing doesn't seem quite right since this works just fine today (though, you have to be careful). Should we have "guard space" and a subset of that as "no, really, this is very guarded" space? That would require extending the guard space a bit further which isn't great...

    One alternative is to manage per-goroutine space for this, but that's not great either, since its more memory used per goroutine.

  8. cherrymui commented on Aug 14, 2020

    @cherrymui
    Member

    Good point. This is especially true for architectures that have a lot registers. Maybe we could decide that some registers are never live across a call?

  9. aclements commented on Aug 14, 2020

    @aclements
    MemberAuthor

    That's an excellent point. A few possible solutions come to mind:

    1. It would be really sad if we had to make more per-goroutine space for this since you almost always have space on the stack. One possibility is to spill to the stack if there's room, and otherwise we keep a pool of allocated "spill space" objects. For that, we could keep just one pre-allocated per P and if the goroutine needs it, it can pull it off the P, spill into it, and then once it's entered the runtime it can allocate a new one (probably getting it from a global allocation pool) and attach it to the P. On return, it can return it to the allocation pool.

    2. A similar option would be to have just one spill space per P. Again, use the stack if you can, otherwise spill into the P, enter the runtime, grow the stack even if it's just a preemption, and then dump the spilled registers back onto the stack in the right place.

    3. We could revisit the nosplit rules. It's often (though not always) a mistake to call from a nosplit function to a non-nosplit function. I'm not sure exactly what this would look like. Maybe a non-nosplit function called from a nosplit function has to have a special prologue? Related: cmd/compile, runtime: add and use go:nosplitrec compilation directive #21314 (comment). I think the only cases I enumerated in that comment that intentionally call from a nosplit to a non-nosplit function aren't running on a user G stack and thus can't grow it anyway.

  10. gopherbot commented on Sep 1, 2020

    @gopherbot
    Contributor

    Change https://golang.org/cl/252258 mentions this issue: cmd/asm: define a macro for GOEXPERIMENT=regabi

  11. gopherbot commented on Sep 1, 2020

    @gopherbot
    Contributor

    Change https://golang.org/cl/252257 mentions this issue: cmd/internal/objabi: add regabi GOEXPERIMENT

  12. gopherbot commented on Oct 1, 2020

    @gopherbot
    Contributor

    Change https://golang.org/cl/258938 mentions this issue: runtime,cmd/cgo: simplify C -> Go call path

  13. 477 remaining items

  14. gopherbot commented on Nov 11, 2025

    @gopherbot
    Contributor

    Change https://go.dev/cl/719481 mentions this issue: internal/bytealg: port bytealg functions to reg ABI on s390x

  15. gopherbot commented on Nov 11, 2025

    @gopherbot
    Contributor

    Change https://go.dev/cl/719482 mentions this issue: cmd/compile, internal/buildcfg: enable regABI on s390x, and add s390x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions