Skip to content

proposal: cmd/compile: define register-based calling convention #18597

Closed

Description

@dr2chase

Performance would be generally somewhat improved if arguments-to and results-from function and method calls used registers instead of the stack. Projected improvements based on a limited prototype are in the range of 5-10%.

The running CL for the prototype: https://go-review.googlesource.com/c/28832/
The prototype, because it is a prototype, uses a pragma that should be unnecessary in the final design, though helpful during development.

(This is a placeholder bug for a design doc.)

problems/tradeoffs noted below, through 2017-01-19 (#18597 (comment))

This will reduce the usefulness of panic tracebacks because it will confuse/corrupt the per-frame argument information there. This was already a problem with SSA and register allocation and spilling, this will make it worse. Perhaps only results should be returned in registers.

Does this provide enough performance boost to justify "breaking" (how much?) existing assembly language?

Given that this breaks existing assembly language, why aren't we also doing a more thorough revision of the calling conventions to include callee-saves registers?

This should use the per-platform ABIs, to make interaction with native code go more smoothly.

Because this preserves the same memory layout of the existing calling, it will consume more stack space than it otherwise might, if that had been optimized out.

And the responses to above, roughly:

In compiler work, 5% (as a geometric mean across benchmarks) is a big deal.

Panic tracebacks are a problem; we will work on that. One possible mitigation is to use DWARF information to make tracebacks much better in general, including properly named and interpreted primitive values. A simpler mitigation for targeted debugging could be an annotation indicating that a function should be compiled to store arguments back to the stack (old style) to ensure that particular function's frame was adequately informative. This is also going to be an issue for improved inlining because regarded at a low level intermediate frames will disappear from backtraces.

The scope of required assembly-language changes is expected to be quite small; from Go-side function declarations the compiler ought to be able to create shims for the assembly to use around function entry, function exit, and surrounding assembly-language CALLs to named functions. The remaining case is assembly-language CALL via a pointer, and these are rare, especially outside the runtime. Thus, the need to bundle changes because they are disruptive is reduced, because the changes aren't that disruptive.

Incorporating callee-saves registers introduces a garbage-collector interaction that is not necessarily insurmountable, but other garbage-collected languages (e.g., Haskell) have been sufficiently intimidated by it that they elected not to use callee-saves registers. Because the assembler can also modify stack frames to include callee-save spill areas and introduce entry/exit shims to save/restore callee-save registers, this appears to have lower impact than initially estimated, and thus we have reduced need to bundle changes. In addition, if we stake out potential callee-save registers early developers can look ahead and adapt their code before it is required.

If each platform's ABI were used instead of this adaptation of the existing calling conventions, the assembly-language impact would be larger, the garbage-collector interactions would be larger, and either best-treatment for Go's multivalue returns would suffer or the cgo story would be sprinkled with asterisks and gotchas. As an alternative (a different way of obtaining a better story for cgo), would we consider a special annotation for cgo-related functions to indicate that exactly the platform calling conventions were used, with no compromises for Go performance?

Activity

  1. added this to the Go1.9Maybe milestone on Jan 10, 2017
  2. self-assigned this
    on Jan 10, 2017
  3. modified the milestones: Proposal, Go1.9Maybe on Jan 10, 2017
  4. gopherbot commented on Jan 10, 2017

    @gopherbot
    Contributor

    CL https://golang.org/cl/35054 mentions this issue.

  5. davecheney commented on Jan 11, 2017

    @davecheney
    Contributor

    5% for amd64 doesn't feel like it justifies the cost of breaking all the assembly written to date.

  6. minux commented on Jan 11, 2017

    @minux
    Member
  7. davecheney commented on Jan 11, 2017

    @davecheney
    Contributor

    I agree with Minux, optimising function calls for 10% speedup vs inlining and all the code generation benefits that unlocks doesn't feel like a good investment.

  8. josharian commented on Jan 11, 2017

    @josharian
    Contributor

    I'm still thinking about the proposal, but I will note that for asm functions with go prototypes and normal stack usage, we can do automated rewrites. Which is also a reminder that the scope for this proposal should include updating vet's asmdecl check.

  9. minux commented on Jan 11, 2017

    @minux
    Member
  10. randall77 commented on Jan 11, 2017

    @randall77
    Contributor

    @minux Yes, we will have all the information necessary to print args as we do now. I suspect it just isn't implemented yet.

  11. minux commented on Jan 11, 2017

    @minux
    Member
  12. randall77 commented on Jan 11, 2017

    @randall77
    Contributor

    The values enter in registers but they will be spilled to the arg slots if they are live at any call. So all live arguments will still be correct. Even modified live arguments should work (except for a few weird corner cases where multiple values derived from the same input are live).

    Dead arguments are not correct even today. What you see in tracebacks is stale. They may get more wrong with this implementation, so it may be worth marking/not displaying dead args.

    Things will get trickier if we allow callee-saved registers. We're thinking about it, but it isn't in the design doc yet.

  13. minux commented on Jan 11, 2017

    @minux
    Member
  14. 54 remaining items

  15. smasher164 commented on Oct 31, 2017

    @smasher164
    Member

    Would the proposed calling convention omit frame pointers for leaf functions? I can see this varying based on whether the function stores the state of a callee-save register, takes the address of a local variable, etc. In that case, is there still a feasible way to obtain the call stack?

  16. wkhere commented on Jan 6, 2018

    @wkhere

    A naive proposal:

    maybe a good optimization would be, instead of changing calling convention, to focus on eliminating local variables on stack by using registers, and sharing as much as possible the stack frame space between variables and arguments/return values of subcalls.

    This way (1) registers would speed up things, (2) stack frames would be smaller, (3) existing calling convention would be preserved.

  17. davecheney commented on Jan 6, 2018

    @davecheney
    Contributor
  18. quasilyte commented on Feb 7, 2018

    @quasilyte
    Contributor

    I can be wrong, but register-based calling convention (CC) could give more performance boost if additional SSA rules are added that actually take advantage of it.

    Currently, a big amount of small and average functions have many mem->reg->mem patterns that make certain optimizations on amd64 not worth it.

    It is not fair to say that 10% is not significant.
    It's 10% with optimizer that works on GOARCH=amd64 like it is GOARCH=386.
    The potential gain is higher.

    Lately, I was comparing 6g with gccgo and even without machine-dependent optimizations, aggressive inlining and constant folding there was about 25-40% performance difference in some allocation-free benchmarks. I do believe that this difference includes CC impact (because most other parts of output machine code look nearly the same).

  19. laboger commented on Feb 8, 2018

    @laboger
    Contributor

    As noted above, the performance benefit for a register-based calling convention is higher on RISC architectures like ppc64le & ppc64. If the above CL is not stale I would be willing to try it out. To me this is one of the biggest performance issues with golang on ppc64le.

    Inlining does help but doesn't handle all cases if there are conditions that inhibit inlining. Also functions written in asm can't be inlined AFAIK.

  20. dr2chase commented on Feb 8, 2018

    @dr2chase
    ContributorAuthor

    The CL is very stale. First we have to get to a good place with debugger information (seems very likely for 1.10 [oops, 1.11]), and then we have to redo some of the experiment (it will go more quickly this time) with a better set of benchmarks. Some optimizations that looked promising on the go1 benchmarks turn out to look considerably less profitable when run against a wider set of benchmarks.

  21. as commented on Feb 9, 2018

    @as
    Contributor

    @dr2chase

    Is the method used to determine the 5% performance improvement somewhere in the discussion? I'm curiously interested in what the function sample space actually looks like.

  22. laboger commented on Feb 9, 2018

    @laboger
    Contributor

    I think the measurement we want is how much does this help once inlining is fully enabled by default.

  23. dr2chase commented on Feb 9, 2018

    @dr2chase
    ContributorAuthor

    The estimate was based on counting call frequencies, looking at performance improvement on a small number of popular calls "registerized", and extrapolating that to the full number of calls.

    I agree that we want to try better inlining first (it's in the queue for 1.11, now that we have that debugging information fixed), since that will cut the call count (and thus reduce the benefit) of this somewhat more complicated optimization. Mid-stack inlining, however, was one of the optimizations that looked a lot less good when applied to the larger suite of benchmarks. One problem with the larger benchmark suite is selection effect -- anyone who wrote a benchmark for parts of their application cares enough about a performance to write that benchmark, and has probably used it already to hand-optimize around rough spots in the Go implementation, so we'll see reduced gains from optimizing those rough spots.

  24. CAFxX commented on Feb 10, 2018

    @CAFxX
    Contributor

    and has probably used it already to hand-optimize around rough spots in the Go implementation

    From my point of view, the goal of having a better inliner is to have more readable/maintainable (less "hand-optimized") code, not really faster code. So that we can build code addressing the problems we're trying to solve, not the shortcomings of the compiler.

  25. ianlancetaylor commented on Aug 12, 2020

    @ianlancetaylor
    Contributor

    There is an updated proposal at #40724, which addresses many of the issues raised here. Closing this proposal in favor of that one.

  26. locked and limited conversation to collaborators on Nov 13, 2021
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

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions