Repository navigation
Optimize the implementation of ref.func - #14683
Conversation
27912b7 to
660281e
Compare
| ;; @001e v3 = load.i64 user16 aligned readonly region2 v2+8 | ||
| ;; @001e v4 = load.i64 notrap aligned readonly region3 v2+24 | ||
| ;; @001e call_indirect sig0, v3(v4, v0) | ||
| ;; @0022 v5 = load.i64 user16 aligned readonly region2 v2+8 | ||
| ;; @0022 v6 = load.i64 notrap aligned readonly region3 v2+24 | ||
| ;; @0022 call_indirect sig0, v5(v6, v0) |
There was a problem hiding this comment.
I've a thread on Zulip about why this isn't optimizing
| // TODO: ideally this (and `can_move`) are something that alias analysis | ||
| // would infer itself rather than relying on the CLIF generator to provide | ||
| // such fine-grained information about loads/stores that can otherwise be | ||
| // inferred. For now though that's expected to be relatively complicated so | ||
| // this is intended to bring benefit in the meantime, although it requires | ||
| // all users of this to manually verify that the memory is indeed | ||
| // `readonly`-compatible. |
There was a problem hiding this comment.
I don't think this is something that can be inferred, because that would require determining whether a given function call writes to the location (or alias region) in question.
I think we may want to, in the limit of time, annotate signatures with the set of alias regions they use (and maybe, in the fullness of time, split that into observe vs mutate), but that is a bit separate from this TODO I think.
There was a problem hiding this comment.
Do you think it's worth having this comment at all? I tried to capture some of the Cranelift meeting discussion in this, but you're right that actually removing the need for this would be a pretty ambitious project
There was a problem hiding this comment.
Sorry for the delayed response. Yeah I think we can remove this comment now, especially given the other changes that have landed or are in-progress clarifying readonly and adding GVN of readonly loads.
| /// 21-bit page offset for ADRP. The immediate is the signed difference | ||
| /// between the 4KiB page of the label and the 4KiB page of the ADRP | ||
| /// instruction, with high 19 bits in bits 23:5 and low 2 bits in bits | ||
| /// 30:29. Equivalent to the `R_AARCH64_ADR_PREL_PG_HI21` relocation. | ||
| AdrpPage21, | ||
| /// Low 12 bits of the label's address in the 12-bit immediate of an ADD | ||
| /// (immediate), bits 21:10. Equivalent to the `R_AARCH64_ADD_ABS_LO12_NC` | ||
| /// relocation. This has the same 4KiB-alignment requirement as | ||
| /// `AdrpPage21`, with which it is paired. | ||
| AddLo12, | ||
| } |
There was a problem hiding this comment.
Can the new relocations be added in a separate PR, just to help split this up and make it easier to review?
There was a problem hiding this comment.
Unfortunately the core change here can't land without this since otherwise tests on aarch64/riscv64 will fail. I can land the change for relocations separately, but then they're not tested until this lands. I split them out into separate commits for this PR, but if you'd like I can land them ahead of time on main
There was a problem hiding this comment.
I think landing them without tests in a separate PR first would have been easier to review (since Github makes it hard to mark files read while reviewing commit by commit) but I think it is fine at this point.
In the last Cranelift meeting, we discussed the semantics of load
merging and the various memory flags, such as `can_move`, `readonly`,
and `notrap`, and which of these permits GVN'ing redundant loads
together.
We concluded that for a load marked only `readonly` (so, may trap and
cannot move), it should still be safe to GVN that load against earlier
identical loads that dominate it. THe current egraph mid-end does not
do this.
The GVN-of-side-effecting-but-idempotent ops is fundamentally gated by
`is_mergeable_for_egraph`, and in that function, we previously
excluded all loads and stores, with the rationale that (i) readonly +
can-move + notrap loads are *truly* pure and will be handled by the
ordinary egraph GVN mechanisms, and (ii) all lother loads and stores
are subject to whatever our alias analysis does.
These loads occupy a middle ground: they cannot float and
rematerialize anywhere (as a pure op could), but alias analysis is
also too weak to do useful things in many situations, e.g. when the
GVN might occur across a callsite. We'd still like to reason about the
fundamental implication of `readonly` on its own: once any load to the
address *has* occurred (at an original location that hasn't been
code-motioned, etc), and if we continue past that point (no trap),
then `readonly` itself implies that the loaded value will never change
-- so we can rewire subsequent identical loads to use the original
value.
This makes the sequence
```
v2 = load.i32 readonly v1
call fn0()
v3 = load.i32 readonly v1
```
properly GVN `v3` onto `v2`.
Addresses the issue observed in bytecodealliance#14683 and discussed in [this Zulip
thread](https://bytecodealliance.zulipchat.com/#narrow/channel/217117-cranelift/topic/.60VMFuncRef.60.20and.20.60.23.5Breadonly.5D.60/with/633147961).
…ance#14907) This is an API change for `MemFlags` to allow disabling flags in addition to disabling them. This came up during bytecodealliance#14683 as a possibly useful primitive to have, and subjectively I've found this sort of builder/setting configuration pretty flexible to use historically too.
Don't actually replace it with anything just yet, but get a whole bunch of `disas` tests blessed here to handle the updated renumbering of libcalls.
This commit moves the implementation of `ref.func` from a libcall to entirely within wasm code. The goal here is to optimize `ref.func` by primarily avoiding costly lookups of function pointers at runtime what's otherwise known statically at compile time. Specifically one of the main costs of `ref.func` is dynamically looking up a function's wasm/array pointers from its `FuncIndex`, but this is statically known at compile time. All data structures are already visible to wasm code (part of the `VMContext`) so no architectural changes were needed, it's instead just moving initialization into compiled code. This does mean that this now duplicates runtime code as well since that cannot be removed, but it's at least noted in both places that these are duplicates of each other. The net result of this is that `ref.func` executes much more quickly than before since `ref.func` is just a handful of loads/stores compared to a full call into the host. The primary motivator here, bytecodealliance#14616, means that initialization of element segments on imported tables, for example, should get much faster. Closes bytecodealliance#8212 Closes bytecodealliance#13295 Closes bytecodealliance#14616
Add a few more relocations to be handled with `LabelUse` and such.
A near `func_addr` on RISC-V 64 is an `auipc` with a `R_RISCV_PCREL_HI20` relocation against the target followed by an `addi` with a `R_RISCV_PCREL_LO12_I` relocation against the `auipc`. This maps both to the preexisting `LabelUse::PCRelHi20` and `LabelUse::PCRelLo12I` so `MachTextSectionBuilder` can resolve them within a text section, which is required for `ref.func` of a defined function on RISC-V 64. Wasmtime resolves the low half against the same target as the high half since `PCRelLo12I` is defined in terms of the final target.
9a01889 to
3a1c924
Compare
…dealliance#14972) * Cranelift: merge readonly (but not notrap or can-move) loads. In the last Cranelift meeting, we discussed the semantics of load merging and the various memory flags, such as `can_move`, `readonly`, and `notrap`, and which of these permits GVN'ing redundant loads together. We concluded that for a load marked only `readonly` (so, may trap and cannot move), it should still be safe to GVN that load against earlier identical loads that dominate it. THe current egraph mid-end does not do this. The GVN-of-side-effecting-but-idempotent ops is fundamentally gated by `is_mergeable_for_egraph`, and in that function, we previously excluded all loads and stores, with the rationale that (i) readonly + can-move + notrap loads are *truly* pure and will be handled by the ordinary egraph GVN mechanisms, and (ii) all lother loads and stores are subject to whatever our alias analysis does. These loads occupy a middle ground: they cannot float and rematerialize anywhere (as a pure op could), but alias analysis is also too weak to do useful things in many situations, e.g. when the GVN might occur across a callsite. We'd still like to reason about the fundamental implication of `readonly` on its own: once any load to the address *has* occurred (at an original location that hasn't been code-motioned, etc), and if we continue past that point (no trap), then `readonly` itself implies that the loaded value will never change -- so we can rewire subsequent identical loads to use the original value. This makes the sequence ``` v2 = load.i32 readonly v1 call fn0() v3 = load.i32 readonly v1 ``` properly GVN `v3` onto `v2`. Addresses the issue observed in bytecodealliance#14683 and discussed in [this Zulip thread](https://bytecodealliance.zulipchat.com/#narrow/channel/217117-cranelift/topic/.60VMFuncRef.60.20and.20.60.23.5Breadonly.5D.60/with/633147961). * Review feedback. * Re-bless new test with better opts
This PR is a series of commits which ends up optimizing the
ref.funcimplementation within Wasmtime to notably not require a hostcall and additionally not require any function lookups within the compiled function tables. There's a number of linked issues to poor performance ofref.funcand while this isn't a truly "zero cost" implementation it is significantly faster than it otherwise is today. The same implementation as before, where the fields are unconditionally initialized, is used no matter what. Conditional initialization could be part of a later runtime refactoring, if needed.