Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WebAssembly] Add RefTypeMem2Local pass #81965

Merged
merged 6 commits into from
Feb 27, 2024
Merged

[WebAssembly] Add RefTypeMem2Local pass #81965

merged 6 commits into from
Feb 27, 2024

Conversation

aheejin
Copy link
Member

@aheejin aheejin commented Feb 16, 2024

This adds WebAssemblyRefTypeMem2Local pass, which changes the address spaces of reference type allocas to addrspace(1). This in turn changes the address spaces of all load and store instructions that use the allocas.

addrspace(1) is WASM_ADDRESS_SPACE_VAR, and loads and stores to this address space become local.gets and local.sets, thanks to the Wasm local IR support added in
82f92e3.

In a follow-up PR, I am planning to replace the usage of mem2reg pass with this to solve the reference type alloca problems described in #81575.

This adds `WebAssemblyRefTypeMem2Local` pass, which changes the address
spaces of reference type `alloca`s to `addrspace(1)`. This in turn
changes the address spaces of all `load` and `store` instructions that
use the `alloca`s.

`addrspace(1)` is `WASM_ADDRESS_SPACE_VAR`, and loads and stores to this
address space become `local.get`s and `local.set`s, thanks to the Wasm
local IR support added in
llvm@82f92e3.

In a follow-up PR, I am planning to replace the usage of mem2reg pass
with this to solve the reference type `alloca` problems described in
 llvm#81575.
@llvmbot
Copy link
Collaborator

llvmbot commented Feb 16, 2024

@llvm/pr-subscribers-backend-webassembly

Author: Heejin Ahn (aheejin)

Changes

This adds WebAssemblyRefTypeMem2Local pass, which changes the address spaces of reference type allocas to addrspace(1). This in turn changes the address spaces of all load and store instructions that use the allocas.

addrspace(1) is WASM_ADDRESS_SPACE_VAR, and loads and stores to this address space become local.gets and local.sets, thanks to the Wasm local IR support added in
82f92e3.

In a follow-up PR, I am planning to replace the usage of mem2reg pass with this to solve the reference type alloca problems described in #81575.


Full diff: https://github.com/llvm/llvm-project/pull/81965.diff

5 Files Affected:

  • (modified) llvm/lib/Target/WebAssembly/CMakeLists.txt (+1)
  • (modified) llvm/lib/Target/WebAssembly/WebAssembly.h (+2)
  • (added) llvm/lib/Target/WebAssembly/WebAssemblyRefTypeMem2Local.cpp (+91)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyTargetMachine.cpp (+1)
  • (added) llvm/test/CodeGen/WebAssembly/ref-type-mem2local.ll (+39)
diff --git a/llvm/lib/Target/WebAssembly/CMakeLists.txt b/llvm/lib/Target/WebAssembly/CMakeLists.txt
index bb2ccea5c14598..f430be2653b4ee 100644
--- a/llvm/lib/Target/WebAssembly/CMakeLists.txt
+++ b/llvm/lib/Target/WebAssembly/CMakeLists.txt
@@ -43,6 +43,7 @@ add_llvm_target(WebAssemblyCodeGen
   WebAssemblyOptimizeLiveIntervals.cpp
   WebAssemblyOptimizeReturned.cpp
   WebAssemblyPeephole.cpp
+  WebAssemblyRefTypeMem2Local.cpp
   WebAssemblyRegisterInfo.cpp
   WebAssemblyRegColoring.cpp
   WebAssemblyRegNumbering.cpp
diff --git a/llvm/lib/Target/WebAssembly/WebAssembly.h b/llvm/lib/Target/WebAssembly/WebAssembly.h
index 91765ad117bdb0..1c40addb6d6f78 100644
--- a/llvm/lib/Target/WebAssembly/WebAssembly.h
+++ b/llvm/lib/Target/WebAssembly/WebAssembly.h
@@ -30,6 +30,7 @@ ModulePass *createWebAssemblyAddMissingPrototypes();
 ModulePass *createWebAssemblyFixFunctionBitcasts();
 FunctionPass *createWebAssemblyOptimizeReturned();
 FunctionPass *createWebAssemblyLowerRefTypesIntPtrConv();
+FunctionPass *createWebAssemblyRefTypeMem2Local();
 
 // ISel and immediate followup passes.
 FunctionPass *createWebAssemblyISelDag(WebAssemblyTargetMachine &TM,
@@ -59,6 +60,7 @@ ModulePass *createWebAssemblyMCLowerPrePass();
 // PassRegistry initialization declarations.
 void initializeFixFunctionBitcastsPass(PassRegistry &);
 void initializeOptimizeReturnedPass(PassRegistry &);
+void initializeWebAssemblyRefTypeMem2LocalPass(PassRegistry &);
 void initializeWebAssemblyAddMissingPrototypesPass(PassRegistry &);
 void initializeWebAssemblyArgumentMovePass(PassRegistry &);
 void initializeWebAssemblyCFGSortPass(PassRegistry &);
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyRefTypeMem2Local.cpp b/llvm/lib/Target/WebAssembly/WebAssemblyRefTypeMem2Local.cpp
new file mode 100644
index 00000000000000..45773fd179184d
--- /dev/null
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyRefTypeMem2Local.cpp
@@ -0,0 +1,91 @@
+//=== WebAssemblyRefTypeMem2Local.cpp - WebAssembly RefType Mem2Local -----===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Assign reference type allocas to local addrspace (addrspace(1)) so that
+/// their loads and stores can be lowered to local.gets/local.sets.
+///
+//===----------------------------------------------------------------------===//
+
+#include "Utils/WasmAddressSpaces.h"
+#include "Utils/WebAssemblyTypeUtilities.h"
+#include "WebAssembly.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/InstVisitor.h"
+#include "llvm/IR/ValueHandle.h"
+#include "llvm/Pass.h"
+using namespace llvm;
+
+#define DEBUG_TYPE "wasm-ref-type-mem2local"
+
+namespace {
+class WebAssemblyRefTypeMem2Local final
+    : public FunctionPass,
+      public InstVisitor<WebAssemblyRefTypeMem2Local> {
+  StringRef getPassName() const override {
+    return "WebAssembly Refernce Types Memory to Local";
+  }
+
+  void getAnalysisUsage(AnalysisUsage &AU) const override {
+    AU.setPreservesCFG();
+    FunctionPass::getAnalysisUsage(AU);
+  }
+
+  bool runOnFunction(Function &F) override;
+  bool Changed = false;
+
+public:
+  static char ID;
+  WebAssemblyRefTypeMem2Local() : FunctionPass(ID) {}
+
+  void visitAllocaInst(AllocaInst &AI);
+};
+} // End anonymous namespace
+
+char WebAssemblyRefTypeMem2Local::ID = 0;
+INITIALIZE_PASS(WebAssemblyRefTypeMem2Local, DEBUG_TYPE,
+                "Assign reference type allocas to local address space", true,
+                false)
+
+FunctionPass *llvm::createWebAssemblyRefTypeMem2Local() {
+  return new WebAssemblyRefTypeMem2Local();
+}
+
+void WebAssemblyRefTypeMem2Local::visitAllocaInst(AllocaInst &AI) {
+  if (WebAssembly::isWebAssemblyReferenceType(AI.getAllocatedType())) {
+    Changed = true;
+    IRBuilder<> IRB(AI.getContext());
+    IRB.SetInsertPoint(&AI);
+    auto *NewAI = IRB.CreateAlloca(AI.getAllocatedType(),
+                                   WebAssembly::WASM_ADDRESS_SPACE_VAR, nullptr,
+                                   AI.getName() + ".var");
+
+    // The below is basically equivalent to AI.replaceAllUsesWith(NewAI), but we
+    // cannot use it because it requires the old and new types be the same,
+    // which is not true here because the address spaces are different.
+    if (AI.hasValueHandle())
+      ValueHandleBase::ValueIsRAUWd(&AI, NewAI);
+    if (AI.isUsedByMetadata())
+      ValueAsMetadata::handleRAUW(&AI, NewAI);
+    while (!AI.materialized_use_empty()) {
+      Use &U = *AI.materialized_use_begin();
+      U.set(NewAI);
+    }
+
+    AI.eraseFromParent();
+  }
+}
+
+bool WebAssemblyRefTypeMem2Local::runOnFunction(Function &F) {
+  LLVM_DEBUG(dbgs() << "********** WebAssembly RefType Mem2Local **********\n"
+                       "********** Function: "
+                    << F.getName() << '\n');
+
+  visit(F);
+  return Changed;
+}
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyTargetMachine.cpp b/llvm/lib/Target/WebAssembly/WebAssemblyTargetMachine.cpp
index 42043a7b8680a4..681e39e5aa5e32 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblyTargetMachine.cpp
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyTargetMachine.cpp
@@ -68,6 +68,7 @@ extern "C" LLVM_EXTERNAL_VISIBILITY void LLVMInitializeWebAssemblyTarget() {
   initializeLowerGlobalDtorsLegacyPassPass(PR);
   initializeFixFunctionBitcastsPass(PR);
   initializeOptimizeReturnedPass(PR);
+  initializeWebAssemblyRefTypeMem2LocalPass(PR);
   initializeWebAssemblyArgumentMovePass(PR);
   initializeWebAssemblySetP2AlignOperandsPass(PR);
   initializeWebAssemblyReplacePhysRegsPass(PR);
diff --git a/llvm/test/CodeGen/WebAssembly/ref-type-mem2local.ll b/llvm/test/CodeGen/WebAssembly/ref-type-mem2local.ll
new file mode 100644
index 00000000000000..8f09b25cca8d05
--- /dev/null
+++ b/llvm/test/CodeGen/WebAssembly/ref-type-mem2local.ll
@@ -0,0 +1,39 @@
+; RUN: opt < %s -wasm-ref-type-mem2local -S | FileCheck %s
+
+target triple = "wasm32-unknown-unknown"
+
+%externref = type ptr addrspace(10)
+%funcref = type ptr addrspace(20)
+
+declare %funcref @get_funcref()
+declare %externref @get_externref()
+declare void @take_funcref(%funcref)
+declare void @take_externref(%externref)
+
+; CHECK-LABEL: @test_ref_type_mem2local
+define void @test_ref_type_mem2local() {
+entry:
+  %alloc.externref = alloca %externref, align 1
+  %eref = call %externref @get_externref()
+  store %externref %eref, ptr %alloc.externref, align 1
+  %eref.loaded = load %externref, ptr %alloc.externref, align 1
+  call void @take_externref(%externref %eref.loaded)
+  ; CHECK:      %alloc.externref.var = alloca ptr addrspace(10), align 1, addrspace(1)
+  ; CHECK-NEXT: %eref = call ptr addrspace(10) @get_externref()
+  ; CHECK-NEXT: store ptr addrspace(10) %eref, ptr addrspace(1) %alloc.externref.var, align 1
+  ; CHECK-NEXT: %eref.loaded = load ptr addrspace(10), ptr addrspace(1) %alloc.externref.var, align 1
+  ; CHECK-NEXT: call void @take_externref(ptr addrspace(10) %eref.loaded)
+
+  %alloc.funcref = alloca %funcref, align 1
+  %fref = call %funcref @get_funcref()
+  store %funcref %fref, ptr %alloc.funcref, align 1
+  %fref.loaded = load %funcref, ptr %alloc.funcref, align 1
+  call void @take_funcref(%funcref %fref.loaded)
+  ; CHECK-NEXT: %alloc.funcref.var = alloca ptr addrspace(20), align 1, addrspace(1)
+  ; CHECK-NEXT: %fref = call ptr addrspace(20) @get_funcref()
+  ; CHECK-NEXT: store ptr addrspace(20) %fref, ptr addrspace(1) %alloc.funcref.var, align 1
+  ; CHECK-NEXT: %fref.loaded = load ptr addrspace(20), ptr addrspace(1) %alloc.funcref.var, align 1
+  ; CHECK-NEXT: call void @take_funcref(ptr addrspace(20) %fref.loaded)
+
+  ret void
+}

@aheejin
Copy link
Member Author

aheejin commented Feb 16, 2024

@xortoast Thanks for the hint! I tried to add you as a reviewer but couldn't, because I guess you don't have a committer access. But I'd appreciate if you take a look.

Comment on lines +68 to +70
// The below is basically equivalent to AI.replaceAllUsesWith(NewAI), but we
// cannot use it because it requires the old and new types be the same,
// which is not true here because the address spaces are different.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't do AI.replaceAllUsesWith(NewAI) because of this assertion:

assert(New->getType() == getType() &&
"replaceAllUses of value with new value of different type!");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the IR verifier still pass after this transformation, or does it also check this invariant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running -verify after this pass doesn't report any error.

@xortoast
Copy link
Contributor

Would it be beneficial to expand this pass to all types (where possible), not just reference types?

The main argument against unconditionally running mem2reg was that it causes debug info to be handled differently or lost. AFAIK this should not be an issue here, since we are keeping all the loads and stores the same and just modifying the allocas.

In fact, this would make debugging easier (especially in situations where DWARF debug info is not available), as well as produce simpler and faster code in unoptimized builds.

@aheejin
Copy link
Member Author

aheejin commented Feb 22, 2024

Sorry for the late reply! I was OOO for a while.

Would it be beneficial to expand this pass to all types (where possible), not just reference types?

The main argument against unconditionally running mem2reg was that it causes debug info to be handled differently or lost. AFAIK this should not be an issue here, since we are keeping all the loads and stores the same and just modifying the allocas.

Correct me if I'm mistaken. allocas debug information is usually tracked with llvm.dbg.declare, and this llvm.dbg.declare info is later stored in the MMI table. So this debug info is handled differently from llvm.dbg.value, which is lowered down to DBG_VALUE mir instruction.

But it looks the MMI table only works with memory, so currently the allocas/stores/loads that are lowered down to local.sets/local.gets seem to lose their debug info, which we someday should fix. For example, if I add llvm.dbg.declare after allocas in this PR's test case, like this:

  %alloc.externref = alloca ptr addrspace(10), align 1, !dbg !17                 
  tail call void @llvm.dbg.declare(metadata ptr %alloc.externref, metadata !9, metadata !DIExpression()), !dbg !17
  ...
  %alloc.funcref = alloca ptr addrspace(20), align 1, !dbg !22                   
  tail call void @llvm.dbg.declare(metadata ptr %alloc.funcref, metadata !14, metadata !DIExpression()), !dbg !22
  ...
  %alloc.i32 = alloca i32, align 4, !dbg !33                                     
  tail call void @llvm.dbg.declare(metadata ptr %alloc.i32, metadata !30, metadata !DIExpression()), !dbg !33

and compile it to an object file and using llvm-dwarfdump to see the info, reftype allocas lose their debug info, while the pod i32 alloca has its debug info retained using the MMI table:

0x0000003e:     DW_TAG_variable
                  DW_AT_location        (DW_OP_fbreg +12)
                  DW_AT_decl_file       ("/ref-type-mem2local.ll")
                  ...

So I'm not sure what you mean by that debug info is not an issue here, because it looks debug info is lost. I think what we should do is to generate appropriate DBG_VALUE instructions in the iSel to match those newly generated local.get/local.sets, but still, unlike the MMI table info, DBG_VALUE info can be lost going through backend transformations. (Even if we disable optimizations, there are still some backend passes we need to run)

Given that reference type variables are supposedly rare, I don't think this loss of debug info would affect the debug info coverage a lot yet, but doing the same thing for all types will likely delete all variable debug info at this point. Even if we add the DBG_VALUE transfer, because that info is not as reliable as the MMI table, they can still be lost.

In fact, this would make debugging easier (especially in situations where DWARF debug info is not available), as well as produce simpler and faster code in unoptimized builds.

Why would it make debugging easier? I also don't understand what situation you mean by "where DWARF debug info is not available", because I've been assuming DWARF info when talking about debug info in LLVM. Are there other framework we use to cover variable debug info?

Also I think it's OK for unoptimized builds to be slow.

@pmatos
Copy link
Contributor

pmatos commented Feb 23, 2024

@aheejin have you tried removing the mem2reg pass and see if things still work? AFAIR, it was mem2reg which held this together and it was forcefully enabled at all optimization levels.

@aheejin
Copy link
Member Author

aheejin commented Feb 23, 2024

@pmatos Yes, replacing the mem2reg with this pass seems to work on all existing LLVM and Emscripten reference type tests. I am planning to upload that, I mean the replacing, as a follow-up to this PR.

@pmatos
Copy link
Contributor

pmatos commented Feb 23, 2024

@pmatos Yes, replacing the mem2reg with this pass seems to work on all existing LLVM and Emscripten reference type tests. I am planning to upload the that, I mean the replacing, as a follow-up to this PR.

Excellent. LGTM. It turned out simpler than what I had imagined. Nice patch.

Copy link
Contributor

@pmatos pmatos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@aheejin
Copy link
Member Author

aheejin commented Feb 27, 2024

@xortoast Do you have remaining concerns?

@xortoast
Copy link
Contributor

@aheejin Sorry for the late reply.

But it looks the MMI table only works with memory.

I was not aware of that.

I also don't understand what situation you mean by "where DWARF debug info is not available".

I was referring to runtimes that don't support DWARF and cases where DWARF debug info is lost due to post-processing. In those cases you can only see raw locals and raw linear memory, and I think it's easier to work with the former.

Are there other framework we use to cover variable debug info?

You can attach names to local variables with the name custom section, but we do not (yet) support that.

In any case, my previous comment was based on some incorrect assumptions. You can merge this PR now, and we can continue this discussion later when the situation with debug info changes.

@aheejin aheejin merged commit d4cdb51 into llvm:main Feb 27, 2024
4 checks passed
@aheejin aheejin deleted the mem2local branch February 27, 2024 22:00
aheejin added a commit to aheejin/llvm-project that referenced this pull request Feb 27, 2024
When reference-types feature is enabled, forcing mem2reg unconditionally
even in `-O0` has some problems described in llvm#81575. This uses
RefTypeMem2Local pass added in llvm#81965 instead. This also removes
`IsForced` parameter added in
llvm@890146b
given that we don't need it anymore.

This may still hurt debug info related to reference type variables a
little during the backend transformation given that they are not stored
in memory anymore, but reference type variables are presumably rare and
it would be still a lot less damage than forcing mem2reg on the whole
program. Also this fixes the EH problem described in llvm#81575.

Fixes llvm#81575.
aheejin added a commit that referenced this pull request Mar 6, 2024
When reference-types feature is enabled, forcing mem2reg unconditionally
even in `-O0` has some problems described in #81575. This uses
RefTypeMem2Local pass added in #81965 instead. This also removes
`IsForced` parameter added in

890146b
given that we don't need it anymore.

This may still hurt debug info related to reference type variables a
little during the backend transformation given that they are not stored
in memory anymore, but reference type variables are presumably rare and
it would be still a lot less damage than forcing mem2reg on the whole
program. Also this fixes the EH problem described in #81575.

Fixes #81575.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants