[IR] Add @llvm.structured.alloca (#186811)

This instruction is an alternative for the `alloca` instruction when
targeting logical targets like DXIL/SPIR-V.
This instruction allocates some memory, but the exact size of the
allocation is not known at the IR level. Only some equivalence can be
determined.

Commit adds docs, instruction declaration, and IR verifier testing.
Related to:
https://discourse.llvm.org/t/rfc-adding-logical-structured-alloca/
This commit is contained in:
Nathan Gauër
2026-03-30 18:06:27 +02:00
committed by GitHub
parent 644b74f926
commit 443e766b40
7 changed files with 144 additions and 13 deletions

View File

@@ -1457,10 +1457,11 @@ Currently, only the following parameter attributes are defined:
present and assign it particular semantics. This will be documented on
individual intrinsics.
The attribute may only be applied to pointer typed arguments of intrinsic
calls. It cannot be applied to non-intrinsic calls, and cannot be applied
to parameters on function declarations. For non-opaque pointers, the type
passed to ``elementtype`` must match the pointer element type.
The attribute may only be applied to pointer typed arguments or return
values of intrinsic calls. It cannot be applied to non-intrinsic calls,
and cannot be applied to parameters on function declarations.
For non-opaque pointers, the type passed to ``elementtype`` must match
the pointer element type.
.. _attr_align:
@@ -15327,6 +15328,86 @@ This is, however, dependent on context that codegen has an insight on. The
fact that `[ i32 x 4 ]` and `%S` are equivalent depends on the target.
.. _i_structured_alloca:
'``llvm.structured.alloca``' Intrinsic
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Syntax:
"""""""
::
declare elementtype(<allocated_type>) ptr
@llvm.structured.alloca()
Overview:
"""""""""
The '``llvm.structured.alloca``' intrinsic allocates uninitialized memory on
the stack for a logical type, such as an aggregate, an array, or a scalar.
Unlike the standard :ref:`alloca <i_alloca>` instruction, the physical memory
layout of a ``llvm.structured.alloca`` is completely opaque to the IR.
Exact padding, size, alignment and subtype offsets is target-dependent and
may differ from the standard ``DataLayout``.
Arguments:
""""""""""
The intrinsic must be annotated with an :ref:`elementtype <attr_elementtype>`
attribute at the call-site on the return value. This attribute specifies the
type of the allocated element.
Semantics:
""""""""""
The ``llvm.structured.alloca`` intrinsic allocates uninitialized memory for a
logical type. Loading from uninitialized memory produces an undefined value.
This intrinsic does not guarantee that the allocated memory will store a value
of the given type, only that it allocates enough space for it on the destination
target. While the type's size and layout are constant (independent of location),
the exact padding and offsets between subtypes are opaque to the IR and are
determined by the target's backend.
The resulting pointer is in the :ref:`alloca address space <alloca_addrspace>`
defined in the :ref:`datalayout string <langref_datalayout>`.
Standard pointer arithmetic (``getelementptr``, ``ptradd``) and lifetime
intrinsics (:ref:`llvm.lifetime.start <int_lifestart>`,
:ref:`llvm.lifetime.end <int_lifeend>`) are permitted on the returned pointer.
However, because the physical layout is opaque, using physical pointer
arithmetic requires the frontend or emitting pass to have explicit knowledge of
the backend's layout rules.
Example:
""""""""
.. code-block:: llvm
%S = type { i32, i32, i32, i32 }
; Allocate one instance of %S on the stack
%ptr = call elementtype(%S) ptr @llvm.structured.alloca()
; Access the second field of the allocated struct
%field_ptr = call ptr @llvm.structured.gep(ptr elementtype(%S) %ptr, i32 1)
%val = load i32, ptr %field_ptr
; Allocate an array of 10 i32s on the stack
%array_ptr = call elementtype([10 x i32]) ptr @llvm.structured.alloca()
; Allocate a single i32 on the stack
%scalar_ptr = call elementtype(i32) ptr @llvm.structured.alloca()
; Although the exact size of 'i32' or '%S' is opaque, it is constant
; for the duration of the module. This allows, for example, reusing
; an allocation slot for two different values of the same type.
%a = call elementtype(float) ptr @llvm.structured.alloca()
%b = call elementtype(float) ptr @llvm.structured.alloca()
; %a and %b are guaranteed to have the same allocation size.
.. _int_get_dynamic_area_offset:
'``llvm.get.dynamic.area.offset``' Intrinsic
@@ -27843,8 +27924,8 @@ object's lifetime.
Arguments:
""""""""""
The argument is either a pointer to an ``alloca`` instruction or a ``poison``
value.
The argument is either a pointer to an ``alloca`` instruction or an
``llvm.structured.alloca`` intrinsic, or a ``poison`` value.
Semantics:
""""""""""
@@ -27855,8 +27936,8 @@ Otherwise, the stack-allocated object that ``ptr`` points to is initially
marked as dead. After '``llvm.lifetime.start``', the stack object is marked as
alive and has an uninitialized value.
The stack object is marked as dead when either
:ref:`llvm.lifetime.end <int_lifeend>` to the alloca is executed or the
function returns.
:ref:`llvm.lifetime.end <int_lifeend>` to the alloca/structured.alloca is
executed or the function returns.
After :ref:`llvm.lifetime.end <int_lifeend>` is called,
'``llvm.lifetime.start``' on the stack object can be called again.
@@ -27884,8 +27965,8 @@ The '``llvm.lifetime.end``' intrinsic specifies the end of a
Arguments:
""""""""""
The argument is either a pointer to an ``alloca`` instruction or a ``poison``
value.
The argument is either a pointer to an ``alloca`` instruction or an
``llvm.structured.alloca`` intrinsic, or a ``poison`` value.
Semantics:
""""""""""
@@ -27895,7 +27976,8 @@ If ``ptr`` is a ``poison`` value, the intrinsic has no effect.
Otherwise, the stack-allocated object that ``ptr`` points to becomes dead after
the call to this intrinsic.
Calling ``llvm.lifetime.end`` on an already dead alloca is no-op.
Calling ``llvm.lifetime.end`` on an already dead alloca/structured.alloca is
no-op.
'``llvm.invariant.start``' Intrinsic
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -132,7 +132,7 @@ def DereferenceableOrNull : IntAttr<"dereferenceable_or_null", IntersectMin,
def DisableSanitizerInstrumentation: EnumAttr<"disable_sanitizer_instrumentation", IntersectPreserve, [FnAttr]>;
/// Provide pointer element type to intrinsic.
def ElementType : TypeAttr<"elementtype", IntersectPreserve, [ParamAttr]>;
def ElementType : TypeAttr<"elementtype", IntersectPreserve, [RetAttr, ParamAttr]>;
/// Flatten function by recursively inlining all calls.
def Flatten : EnumAttr<"flatten", IntersectPreserve, [FnAttr]>;

View File

@@ -1804,6 +1804,21 @@ public:
CreateLoop(BasicBlock &BB, ConvergenceControlInst *Parent);
};
class StructuredAllocaInst : public IntrinsicInst {
public:
static bool classof(const IntrinsicInst *I) {
return I->getIntrinsicID() == Intrinsic::structured_alloca;
}
static bool classof(const Value *V) {
return isa<IntrinsicInst>(V) && classof(cast<IntrinsicInst>(V));
}
Type *getAllocationType() const {
return getRetAttr(Attribute::ElementType).getValueAsType();
}
};
class StructuredGEPInst : public IntrinsicInst {
public:
static bool classof(const IntrinsicInst *I) {

View File

@@ -1034,6 +1034,8 @@ def int_structured_gep
[LLVMMatchType<0>, llvm_vararg_ty],
[IntrNoMem, IntrSpeculatable]>;
def int_structured_alloca : DefaultAttrsIntrinsic<[llvm_anyptr_ty], [], [IntrInaccessibleMemOnly]>;
//===------------------- Standard C Library Intrinsics --------------------===//
//

View File

@@ -6978,6 +6978,11 @@ void Verifier::visitIntrinsicCall(Intrinsic::ID ID, CallBase &Call) {
}
break;
}
case Intrinsic::structured_alloca:
Check(Call.hasRetAttr(Attribute::ElementType),
"@llvm.structured.alloca calls require elementtype attribute.",
&Call);
break;
case Intrinsic::amdgcn_cs_chain: {
auto CallerCC = Call.getCaller()->getCallingConv();
switch (CallerCC) {
@@ -7255,7 +7260,9 @@ void Verifier::visitIntrinsicCall(Intrinsic::ID ID, CallBase &Call) {
case Intrinsic::lifetime_start:
case Intrinsic::lifetime_end: {
Value *Ptr = Call.getArgOperand(0);
Check(isa<AllocaInst>(Ptr) || isa<PoisonValue>(Ptr),
IntrinsicInst *II = dyn_cast<IntrinsicInst>(Ptr);
Check(isa<AllocaInst>(Ptr) || isa<PoisonValue>(Ptr) ||
(II && II->getIntrinsicID() == Intrinsic::structured_alloca),
"llvm.lifetime.start/end can only be used on alloca or poison",
&Call);
break;

View File

@@ -0,0 +1,17 @@
; RUN: llvm-as < %s | llvm-dis
%S = type { i32, i32 }
define void @simple_scalar_allocation() {
entry:
; CHECK: %ptr = call elementtype(i32) ptr @llvm.structured.alloca.p0()
%ptr = call elementtype(i32) ptr @llvm.structured.alloca()
ret void
}
define void @struct_allocation() {
entry:
; CHECK: %ptr = call elementtype(%S) ptr @llvm.structured.alloca.p0()
%ptr = call elementtype(%S) ptr @llvm.structured.alloca()
ret void
}

View File

@@ -0,0 +1,8 @@
; RUN: not llvm-as -disable-output %s 2>&1 | FileCheck %s
define void @missing_attribute() {
entry:
; CHECK: @llvm.structured.alloca calls require elementtype attribute.
%ptr = call ptr @llvm.structured.alloca()
ret void
}