Skip to content

Optimize movz/movk on ARM64 #34732

Description

@kunalspathak

Since ARM64 has an ISA with fixed 32-bit instruction width, the move instructions have space for 16-bit unsigned immediate. To move bigger immediate value, we need to move the value in multiple steps using chunks of 16-bits (movz/movk). Due to this, multiple mov instructions are generated to load a single bigger value in register in contrast to x64 where a single mov can load bigger immediate.

I wanted to see if it has been already evaluated or if it is feasible to store such immediate values in a literal pool and load the values from the pool using ldr instead of moving using multi-step movz/movk. I know this would involve accessing memory instead and might end up being slower. In fact, the examples I have tried on clang/gcc, they use movz/movk mostly.

I have also noticed cases where we try to move 2 immediates back to back that are just bytes apart from each other. E.g. Below is the code generated for Vector4.Add(Vector4, Vector4) method where it tries to load the parameters before performing fadd.

        D29D9900          movz    x0, #0xecc8
        F2A785E0          movk    x0, #0x3c2f LSL #16
        F2C051C0          movk    x0, #654 LSL #32
        F9400000          ldr     x0, [x0]            # <==== loads from address 0x6543c2fecc8
        FD400410          ldr     d16, [x0,#8]
        D29D9800          movz    x0, #0xecc0
        F2A785E0          movk    x0, #0x3c2f LSL #16
        F2C051C0          movk    x0, #654 LSL #32    # <==== loads from address 0x6543c2fecc0
        F9400000          ldr     x0, [x0]
        FD400411          ldr     d17, [x0,#8]
        0E31D610          fadd    v16.2s, v16.2s, v17.2s

The 2nd set of move instructions can be optimized by doing sub x0, x0, 8.

Just to get the stats to see how often we need to generate higher immediate values, I ran crossgen tool on all (or most) of the libraries and dumped the assembly code produced from it for ARM64. Then I extracted out the methods and the portion of code that uses movz/movk pair to load the immediates. There were total 191028 methods crossgened out of which 4578 methods contained pair of movz/movk. In all there were 11856 groups of movz/movk instructions.
You can see the summary in movs.txt.

Some of the questions I am thinking about on seeing the above results:

  1. Some of the Utf8Parser methods loads constant multiple times and can it be optimized to reuse the value?
  2. Is it possible to re-evaluate constant propagation for certain scenarios where the value of constant is big and since it gets propagated, we end up creating these mov sequence multiple times?
  3. Should we tweak our inline heuristics because of the amount of code generated for ARM64 is more than that for Intel?
  4. Can we eliminate some of the movz/movk by doing arithmetic manipulation, the way I mentioned in above example?
  5. And finally, is it beneficial to use literal pool table for certain scenarios?

References:

category:cq
theme:optimization
skill-level:intermediate
cost:medium

Metadata

Metadata

Assignees

Labels

arch-arm64area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions