bytecodealliance / bytecodealliance/wasmtime

Cranelift AArch64: Expand the set of allocatable general-purpose registers

Open
#4,552 0 comments 0 reactions 0 assignees View on GitHub
cranelift cranelift:area:aarch64 cranelift:goal:optimize-speed enhancement performance
Dominant language
Rust
Stars
18.6k
Forks
1.8k
Avg merge
1d 18h
Merged PRs (30d)
126

Description

Currently (as of commit 8137432e67c920b73a3bbcc4eb72ae5095d31f41) Cranelift's AArch64 backend [excludes](https://github.com/bytecodealliance/wasmtime/blob/8137432e67c920b73a3bbcc4eb72ae5095d31f41/cranelift/codegen/src/isa/aarch64/inst/regs.rs#L167) the following general-purpose registers (GPRs) unconditionally from the set of allocatable registers:
* `X16`
* `X17`
* `X18`
* `X29` AKA `FP`
* `X30` AKA `LR`
* `X31` AKA `XZR`/`SP`

This list might be too conservative, except for the following cases:
* `X29` - the Procedure Call Standard for the Arm® 64-bit Architecture (AAPCS64) [specifies](https://github.com/ARM-software/abi-aa/blob/2022Q1/aapcs64/aapcs64.rst#the-frame-pointer) that it is the frame pointer and that it must have a valid value at all times
* `X31` - in most contexts (e.g. non-memory operations) it is decoded as `XZR`, which has an architecturally fixed value (0), and in other cases as `SP`, so it is not usable in general

As for the rest:
* `X16` and `X17`

Currently reserved as spill temporaries, as [explained](https://github.com/bytecodealliance/wasmtime/pull/4521#issuecomment-1195717857) by @cfallin:
> regalloc2 actually doesn't need any temporaries anymore, but aarch64 itself does. The reason is that a spillslot may be at a greater offset from `sp` or `fp` than we can reach with an `imm12`, so we need a sequence of instructions to synthesize the address of a spillslot before spilling or reloading. That sequence itself can't require spilling another register if all registers are full (as they are likely to be if we're spilling in the first place), so we need to set aside `x16` for that.
>
> If I recall correctly, `x17` is used in stack-limit check sequences...

From the same discussion, @cfallin's suggestion for an alternative approach:
> ... an alternative approach would be to reserve a small-offset slot to spill another victim to if we need to compute a spillslot address at a large distance away -- so we can bootstrap our way there with no registers initially free.

In particular, we might expand the stack area next to the frame record - instead of decrementing the stack pointer by 16 bytes to save `FP` and `LR` in a function prologue, we could decrement by 32 bytes, so that we would have a scratch area for 2 GPRs as well.

Another option is to reserve a vector register, which would give the same amount of space.

Note that the code generating branch veneers [assumes](https://github.com/bytecodealliance/wasmtime/blob/8137432e67c920b73a3bbcc4eb72ae5095d31f41/cranelift/codegen/src/isa/aarch64/inst/mod.rs#L2807) that both registers are available, so it would need adjustments.

* `X18`

The AAPCS64 [states](https://github.com/ARM-software/abi-aa/blob/2022Q1/aapcs64/aapcs64.rst#general-purpose-registers) that the platform can use it to carry inter-procedural state, which is assumed by Cranelift, but `X18` could be used as a regular temporary register otherwise; perhaps we could revisit that assumption?
* `X30`

PR #4469 introduced the `preserve_frame_pointers` flag, which when true guarantees that the `LR` register is saved in the function prologue and restored in the epilogue, thus making it usable as a temporary register in between; we just have to ensure that calls are set up to clobber it, so that regalloc does the right thing. A restricted version of this idea that is potentially easier to implement is to make `X30` a spill temporary instead, and to turn either `X16` or `X17` into a regular temporary register.

In fact the same optimization is also applicable when the `preserve_frame_pointers` flag is false as long as we are compiling a function that creates a frame record on the stack, e.g. a non-leaf one, but currently the backend plumbing is not set up to make that decision on a per-function basis. As @cfallin stated, any backend changes to remedy that limitation are subject to the following constraint:
> The only thing I want to hold as a hard requirement is that we don't build it dynamically per-function (because there are lots of tiny functions and that would be a nontrivial cost); right now we build it once when the compiler backend is constructed. We could perhaps build a few versions of it though, and return the right one in the `regalloc2::Function` trait -- one for leaf functions and one without; and variations based on compiler flags.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.