[RFC] plan for eliminating `AllocaInst->getAllocatedType`, leaving only `getArraySize`

Since the removal of typed pointers, there has been increasingly little meaning to the alloca element type, and some passes that cannot accurately preserve this element type at all. There are many parallels here between GEP vs ptradd, under the opaque pointer scheme, so I don’t want to repeat all of that discussion and will instead point to https://discourse.llvm.org/t/rfc-replacing-getelementptr-with-ptradd/68699 and https://discourse.llvm.org/t/rfc-de-type-ification-of-llvm-ir-why/88257. Notably, the mere existence of the alloca element type adds awkward complications to passes like SROA and MemCpyOpt, even though they just fallback to byte arrays anyways, so byte array support is already required everywhere. I would like to propose that LLVM discard this legacy function in favor of making an alloca simply a byte-sized object (actually an optional, preserving scalable ability, to be precise). In legacy terms, I propose that all alloca would become arrays like so: alloca i8, i64 %N or alloca < 1 x i8 x vscale>, i64 imm %N, with getAllocatedType returning i8 (until deleted) and isArrayAllocation returning true (until deleted). The proposed IR would be something like alloca i64 %N, align M or alloca vscale x i64 <constant>, align N, once the transition period is complete.

This RFC is intended to draw awareness towards this specific aspect of IR evolution, and the current perceived status of the work.

The reason is as follows: there seems to be no remaining semantic meaning to the alloca->getAllocatedType() function, as any semantic meaning would have inhibited SROA and MemCpyOpt from operating in many important cases. Instead, all semantic information is contained in the Load and Store instructions used, and in TBAA metadata if defined. Thus all optimization passes are supposed to disregard the type info from alloca, other than to compute its size. Indeed, often there is not even a way to preserve the original answer to that query in any meaningful way because of conflicting answers. Thus, across the whole LLVM codebase, there are not many uses of this function left. I’ve recently worked to further shrink that list (making Claude do a lot of the basic refactoring work). Now that almost all of that is done, I’ve cataloged all the remaining uses (Claude made the initial list format and chose to use emojis, but I reviewed and corrected it). The uses fall into 4 general categories:

  1. Simple structural queries that simply get deprecated directly and deleted when getAllocatedType is removed.
  2. Trivially removable queries that get replaced by the allocated size simply.
  3. Passes that try to derive some semantic meaning from the alloca type, which is therefore usually inconsistent with the langref in some way (e.g. attempts to assign a meaning to something which is not in strict accordance with the langref). This needs some case-by-case review and discussion to clarify what the intent is, and make that intent explicit in the langref and/or pass.
  4. GPU backends use that need more discussion & reviewers.

Some transition details:

  1. IRBuilder : We already get the alignment from the DataLayout, so for the existing constructor, this seems it would be would be easy to get the TypeSize there too, and also emit a multiply with the ArraySize argument if needed. There would also be a new constructor needed that takes a Value *ArraySize and bool vscale=false. There could also be a convenience function that takes a TypeSize.
  2. AllocaInst : The constructor would change to take a TypeSize instead of an element type.
  3. Bitcode upgrade : Just like IRBuilder, this already uses the DataLayout for auto-upgrading the alignment, so this seems easy to rewrite there to a ConstantInt + optional-ArraySize-Multiply-Instruction + isScalableTy instruction flag.

Some questions:

  1. What does correct SPIRV and DXIL IR look like? There seems to be several possible options here, but it is not entirely clear which are supported most reliably by the respective backends. This likely shares most of the same issues as the ptradd transition.
  2. What is the correct resolution for each of the uses I called “problematic”? These are generally cases where it was unclear to me what semantics were being imposed here, so they may have a slightly different resolution depending on the original design intent. These are particularly the places where help is needed, either to suggest or review proposed changes.

Detailed File Analysis

1. Core IR Infrastructure

These are expected to be trivially removable, since they are non-semantic uses dealing with the structure of allocating IR.
These would probably change to just [ n x i8 x vscale ] or are simply deleted.

Location Usage Description
Instructions.h:122 Type *getAllocatedType() const { return AllocatedType; } API definition
Instructions.cpp:66 TypeSize Size = DL.getTypeAllocSize(getAllocatedType()) Used with array size multiplication
Instructions.cpp:4366 new AllocaInst(getAllocatedType(), ...) Clone operation
Verifier.cpp:4700 Type *Ty = AI.getAllocatedType() Checking type properties
Verifier.cpp:6239 !AI->getAllocatedType()->isPointerTy() Type property check
IR/AsmWriter.cpp 4740 TypePrinter.print(AI->getAllocatedType(), Out)
IR/Core.cpp 3288 wrap(unwrap<AllocaInst>(Alloca)->getAllocatedType())
IR/TypeFinder.cpp 82 incorporateType(AI->getAllocatedType())
Bitcode/Writer/ValueEnumerator.cpp 482 EnumerateType(AI->getAllocatedType())
Bitcode/Writer/BitcodeWriter.cpp 3447 Vals.push_back(VE.getTypeID(AI.getAllocatedType()))
ValueMapper.cpp 1056 AI->setAllocatedType(TypeMapper->remapType(...))
SandboxIR/Instruction.cpp 1414-1415 Wrapper implementation
SandboxIR/Instruction.h 2310 Wrapper declaration
Tests 5267-5279 Test code

2. Trivially removable

These are uses that are just dealing with converting types to sizes, and so they get easily simplified once sizes are not based on types.

Location Line Usage Description
IR/IRBuilder.cpp 213 TypeSize ElemSize = DL.getTypeAllocSize(AI->getAllocatedType()) Conversion helper
Instruction.cpp:901 AI->getAllocatedType() == cast<AllocaInst>(I2)->getAllocatedType() Type equality check
FunctionComparator.cpp 694-695 cmpTypes(AI->getAllocatedType(), ...) Type comparison
Evaluator.cpp 346 Type *Ty = AI->getAllocatedType() Creating a sized memory region
Interpreter/Execution.cpp 984 Type *Ty = I.getAllocatedType() Type for allocation size
MemoryTaggingSupport.cpp 219-222 Info.AI->getAllocatedType() for padding Creating a sized memory region
SROA.cpp 5176, 5238 Type partitioning logic Just needs sizes
MemCpyOptimizer.cpp 1706 SrcAlloca->setAllocatedType(DestAlloca->getAllocatedType()) Just needs sizes
AddressSanitizer.cpp 1199, 1439 isFixedSize computations Just needs whether there is a static size
InstCombineLoadStoreAlloca.cpp 201 ArrayType::get(AI.getAllocatedType(), C->getZExtValue()) Canonicalization
ExpandVariadics.cpp 760 assert(Alloced->getAllocatedType() == VarargsTy) Type assertion
SelectionDAG/SelectionDAGBuilder.cpp 4621 Type *Ty = I.getAllocatedType() Computing size for codegen
GlobalISel/IRTranslator.cpp 3200 Type *Ty = AI.getAllocatedType() Computing size for codegen
InlineCost.cpp 1595 Type *Ty = I.getAllocatedType() Type for size computation
AMDGPULowerBufferFatPointers.cpp 556 Type *Ty = I.getAllocatedType() Type remapping
AArch64ISelLowering.cpp 30904 AI->getAllocatedType()->isScalableTy() Type property check becomes getAllocationSize–>isScalable
docs/OpaquePointers.rst Documentation
docs/tutorial/MyFirstLanguageFrontend/LangImpl07.rst Tutorial
examples/Kaleidoscope/Chapter7/toy.cpp Tutorial

3. Problematic Passes

These each need to be addressed in some way before removing getAllocatedType. Each has something that seemed a bit complicated to untangle the intention:

Location Line Usage Description
IR/DebugInfo.cpp 2148 DL.getTypeSizeInBits(AI->getAllocatedType()) Semantically awkward use of alloca size to determine what are all the debug information that might be attached
Local.cpp 1772, 1777 AI->getAllocatedType()->isArrayTy(), ->isStructTy() Buggy debug_value handling: Proposed [Utils] Examine debug info type instead of alloca type to guess the debug behavior of the alloca uses by vtjnash · Pull Request #177480 · llvm/llvm-project · GitHub to preserve the behavior perhaps intended. There is a (very) long-term project to delete this code.
AttributorAttributes.cpp 7784 return AI->getAllocatedType() Needs plan. One proposal: [RFC][Attributor] change argument privatization to size-based type selection by vtjnash · Pull Request #181716 · llvm/llvm-project · GitHub
StackProtector.cpp 504 ContainsProtectableArray(AI->getAllocatedType(), ...) Applies stackprotector if there is an alloca defined to contain [ N x i8 ]. This needs to be a front-end job instead (e.g. deprecate ssp, and have the front end decide whether to use sspreq or not), since the LangRef uses the front-end term “Character arrays” to define this attribute, which is not a part of the LLVM IR lexicon. We actually hit this problem in Rust which uses alloca [N x i8] for everything, so the IR type heuristics are just completely broken.
GCRootLowering.cpp 181 cast<PointerType>(Root->getAllocatedType()) The LangRef doesn’t exactly specify what operation happens here or how many bytes are permitted to be accessed. It could be a memset 0 or a nullptr in any addrspace, or the target addrspace could be added as an imm argument? It might make sense to add an elementtype attribute to the pointer argument. Not sure.
StackSafetyAnalysis.cpp 155 DL.getTypeAllocSize(AI.getAllocatedType()) Mostly just computing the size, but returns the element size if the number is unknown, although that seems neither a necessary nor sufficient condition to ensure the number is not zero. So maybe this just gets deleted.
MemoryBuiltins.cpp 955, 1341, 1345 Size calculations Identical to StackSafetyAnalysis but with corrected handling of array size known estimates, making this somewhat difficult to model to preserve this primitive inference functionality exactly.
clang/lib/CodeGen/CGObjCMac.cpp 4334, 4347 InlineAsm requirements This inline asm site doesn’t have an assembly, but SelectionDAG seems to need this element type to decide which register could hold the entire contents of the alloca.
SROA.cpp 5176 getTypePartition llvm-project/llvm/lib/Transforms/Scalar/SROA.cpp at 8f378ea7e6fa31179266c69368be56a866b631e1 · llvm/llvm-project · GitHub the type here seems to influence the promotion strategy that is being used, and in particular which loads and stores are possible to compute an appropriate bitcast strategy for them once hoisted. The existence of FCA, and more significantly, the existence of pointer provenience / non-integral pointers can make this very tricky to make sure SROA doesn’t accidentally invent an illegal operation based on conflicting or confusing semantic information which might even be in dead code.

4. In-Progress PRs

I’ve already merged many PRs, so there’s just a couple big ones that I still have open right now:

  1. #178436 - [CodeGen] Use byte offsets and ptradd in ShadowStackGCLowering

    • Covers: llvm/lib/CodeGen/ShadowStackGCLowering.cpp:200
  2. #178359 - [Coroutines] Replace struct alloca frame with byte array and ptradd

    • Covers: llvm/lib/Transforms/Coroutines/CoroFrame.cpp (5 uses)
  3. #181845 - OMPIRBuilder.cpp | 8 trivial uses, but a lot of tests to fix that relied on matching a typed GEP | Struct type access, Load types |

5. Problematic Backends

WebAssembly

This backend is currently awkwardly mutating the IR just before isel to fix a particular semantic issues from clang: there are two alloca addrspaces and some values (specifically, reference and function pointer types) can only be placed in one or the other, but currently clang cannot correctly choose which one to use, so instead LLVM reimplemented RAUW by hand to avoid the validator noticing that this is not a simple IR transform to do legally.

I have up a series of PRs to change the clang frontend to emit them to the correct alloca addrspace originally, so that LLVM can delete the IR mutation pass which currently relies on getAllocatedType. To wit:

If those changes are accepted, then that work culminates in:

If not acceptable, then we could instead inject more special cases to EmitParmDecl like other backends already do, but I’d prefer to eventually move those into Sema instead.

File Line Usage
WebAssemblyRefTypeMem2Local.cpp 60, 64 Type checks and cloning
WebAssemblyFrameLowering.cpp 72 ComputeValueVTs(..., AI->getAllocatedType(), ...)

AMDGPU

I think this seems to be smarter copy of the mem2reg/sroa pass, which can take an alloca accessed with non-constant GEP and rewrite it to a vector extractelement instead. I would hazard a guess that this probably does not need to initialize that pass info with the alloca type (consistent with the same change to SROA/mem2reg), since it then looks at all the loads and stores and make sure they have the same type as the alloca. It might even be an interesting pass to extract entirely from AMDGPU and make it its own LLVM pass (such as https://github.com/llvm/llvm-project/pull/165159)

File Usage
AMDGPUPromoteAlloca.cpp analyzePromoteToVector

SPIRV

There are many lines of code here to try to assign a possible load and store type to each offset in the alloca. Perhaps this could perhaps be shared with the other places that do the same analysis. How exactly to do that should perhaps be a separate discussion thread, since I know there’s a lot of work going on here already.

File Usage
SPIRVEmitIntrinsics.cpp maybeAssignPtrType(..., Ref->getAllocatedType(), ...)

DirectX

There’s probably a lot of different things going on here with getAllocatedType usage. One issue here is simply that it uses a copy of the LLVM 3.7 bitcode writer which needs something put in that field. Secondly there is code to rewrite alloca types to something simpler that DXIL can handle. Lastly, there is code to figure out what types are being used as loads and stores, similar to SPRIV. Similar to SPIRV, this might all break down once GEP becomes ptradd. But the inference from uses to alloca type might be able to be shared too between these backends, particularly once LLVM removes the last vestiges of typed GEP and typed alloca.

3 Likes

Hi!

Thanks for this RFC. As you rightly mentioned, the SPIR-V backend will have issues with this.
But I agree removing the type of the alloca would make it more consistent with the ptradd semantics.
I think the best path forward is to do the same as the getelementptr instruction: create two paths:

  • one untyped (ptradd/alloca)
  • one logical/typed (structured.gep, structured.alloca)

I’ve opened a “sister” RFC to suggest adding a typed alloca intrinsic ([RFC] Adding logical/structured alloca). If we can go this way, I’m fine removing the type on the alloca instruction.

Thanks for the proposal. I believe that this aligns well with our general direction for IR evolution. On the Rust side, we’ve been emitting all allocas in [N x i8] form for a while now, and the only issue we’ve ever encountered with that is the interaction with stack protector heuristics that you already mentioned. That’s pretty strong evidence that the alloca type information is, indeed, redundant.