# \[RFC\] \`@llvm.parallel\_alloca\` intrinsic for vectorizable stack allocations

**URL:** <https://discourse.llvm.org/t/rfc-llvm-parallel-alloca-intrinsic-for-vectorizable-stack-allocations/90106>\
**Category:** Loop Optimizations\
**Tags:** llvm\
**Created:** [March 9, 2026, 4:25pm UTC](https://discourse.llvm.org/t/rfc-llvm-parallel-alloca-intrinsic-for-vectorizable-stack-allocations/90106 "2026-03-09T16:25:38Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![juliusikkala](https://sea1.discourse-cdn.com/flex021/user_avatar/discourse.llvm.org/juliusikkala/32/26596_2.png) [@juliusikkala](https://discourse.llvm.org/u/juliusikkala)\
**Post date:** [March 9, 2026, 4:25pm UTC](https://discourse.llvm.org/t/rfc-llvm-parallel-alloca-intrinsic-for-vectorizable-stack-allocations/90106/1 "2026-03-09T16:25:38Z")

</div>

This RFC proposes the addition of a new LLVM intrinsic function, `@llvm.parallel_alloca`. Similarly to `alloca`, this intrinsic allocates stack memory. However, it is not hoistable and can be vectorized such that each vector lane receives a unique pointer.

# Motivation

`alloca` instructions are typically assumed to be hoistable into the entry block of the function, either directly when the frontend is emitting them (e.g. Clang) or during inlining. If you declare a variable in a loop, the related `alloca` is placed in the entry block of the function instead of inside the loop.

Usually, this behavior is fine. The lifetime of the allocation doesn’t overlap between loop iterations when the loop is executed serially. As multiple iterations don’t simultaneously access the allocation, the same memory can be shared between all iterations.

However, if the loop is vectorized, multiple iterations may run in parallel. The hoisted `alloca` may now introduce a memory dependency between iterations that did not need to exist, and can prevent vectorization. Worse yet, if the loop is annotated with `@llvm.loop.parallel_accesses` (e.g. via `#pragma clang loop vectorize(assume_safety)`), this dependency is ignored and the program behaves incorrectly.

The latter behavior is particularly insidious when combined with inlining:

```c
void helperFunc(int *A, int i) {
    int t[16];
    // Fill entire local array with 'i'
    t[0] = i; t[1] = i; t[2] = i; t[3] = i;
    t[4] = i; t[5] = i; t[6] = i; t[7] = i;
    t[8] = i; t[9] = i; t[10] = i; t[11] = i;
    t[12] = i; t[13] = i; t[14] = i; t[15] = i;
    // Just read one of them, it has to be 'i'. Right? Right???
    A[i] = t[i];
}

void foo(int *A, int n) {
    #pragma clang loop vectorize(assume_safety)
    for (int i = 0; i < n; ++i) {
        // This call gets inlined.
        helperFunc(A, i);
    }
}

```

As the `alloca` for the `t` array is actually hoisted into the entry block of `foo` during inlining, the above example causes big problems. Multiple iterations are simultaneously writing into the same allocation for `t`. If `foo` is called with `n = 16`, `A` is `7 7 7 7 7 7 7 7 15 15 15 15 15 15 15 15` on my machine.

Worse yet, it’s not only `vectorize(assume_safety)` that uses the `@llvm.loop.parallel_accesses` metadata. [`#pragma omp simd`](https://github.com/llvm/llvm-project/issues/179272) is also affected. I believe this breaks the semantics of OpenMP.

The reason why this behavior doesn’t always break nearly every program using those pragmas is that the Mem2Reg pass usually promotes allocas into registers, which do get vectorized safely. However, if that doesn’t happen for one reason or another, like here, we can be in big trouble.

# Method

In general, we can’t demand passes to be aware of metadata for correctness, so we can’t demand that `alloca` can never be hoisted from parallel loops. Loops are marked as parallel using the `llvm.loop.parallel_accesses` metadata, and LLVM passes shouldn’t have to be aware of metadata to produce correct code.

Instead, we propose adding a new intrinsic for emitting parallel-safe stack allocations:

```auto
<N x ptr> @llvm.parallel_alloca(i64 immarg size, i64 immarg align)
<N x ptr> @llvm.parallel_alloca(i32 immarg size, i32 immarg align)

```

This intrinsic is mostly a drop-in replacement for `alloca`, but with certain semantic differences that make it behave well in parallel loops.

The user can do e.g. `call ptr @llvm.parallel_alloca(64, 8)` inside a parallel loop. This can be treated similarly to a regular `alloca [64 x i8], align 8`, except that:

- It must be treated as if it is not speculatable and has side effects.
- It can be vectorized to return multiple pointers and allocate the same amount of memory for each vector lane.

If this intrinsic is called outside of a parallel loop, it can be treated as a normal `alloca` and lowered as such. If the intrinsic is lowered into an `alloca` in a parallel loop, the parallel loop must be demoted into a sequential loop. However, the intended behavior is that the intrinsic is not lowered until after loop vectorization has run and potentially vectorized the intrinsic.

For a single pointer return value, `ptr @llvm.parallel_alloca(s, a)` lowers into an `alloca [s x i8], align a` in the entry block. The address space of the `alloca` is determined from the address space of the returned pointer type of `@llvm.parallel_alloca`.

For a vector of pointers, `<N x ptr> @llvm.parallel_alloca(s, a)` is lowered as below:

```auto
; In entry block:
; K = roundToNextMultipleOf(s, a)
%1 = alloca [(N * K) x i8], align a
%2 = getelementptr inbounds i8, ptr %1, <0, K, 2K, 3K, ...>
; replace all uses of the original @llvm.parallel_alloca call with %2.

```

This effectively “privatizes” the allocation for each vector lane; each lane gets a unique pointer with at least the given amount of bytes with the specified alignment.

Lifetime annotations are not extended to accept pointers from `@llvm.parallel_alloca` to limit the impact of this RFC. The allocated memory is freed similarly to `alloca` with no lifetime annotations; it is only freed when the function that called the `parallel_alloca` intrinsic returns.

# Impact

Areas of the compiler utilizing the parallel metadata may be affected. That is to say, if a pass is aware of `llvm.loop.parallel_accesses` and `llvm.access.group`, it also has to be aware of `@llvm.parallel_alloca` in the future.

The general rule is that code behaves correctly if it is either fully unaware of parallel metadata, or fully aware. This new intrinsic is an extension to the existing parallel metadata.

There are very few passes that currently utilize parallel metadata, so we expect the impact to be minor.

# Implementation

- Add a utility function `lowerParallelAlloca` for lowering `@llvm.parallel_alloca` into a normal `alloca` in the caller’s entry block, as specified in the Method section above.

- `InlineFunction` should upgrade `alloca` into `@llvm.parallel_alloca` when inlining a function call with `@llvm.access.group` metadata.

- Clang should emit `@llvm.parallel_alloca` instead of `alloca` when creating temporary variables inside a parallel loop.

- Mem2Reg should allow promoting `@llvm.parallel_alloca`

- LoopVectorize should call `lowerParallelAlloca` immediately after vectorizing it and strip parallel metadata from the loop.

- PreISelIntrinsicLowering needs to lower `@llvm.parallel_alloca` if no prior pass lowered it yet

* * *

Authors: @juliusikkala (me), @pjaaskel (Pekka Jääskeläinen).

There is a work-in-progress implementation in [my branch.](https://github.com/juliusikkala/llvm-project/commits/parallel-alloca-intrinsic/)

Prior related discussions:

- [Issue with `#pragma omp simd`](https://github.com/llvm/llvm-project/issues/179272)
- [Discourse thread of prior fix attempt](https://discourse.llvm.org/t/semantics-of-llvm-loop-parallel-accesses-and-interaction-with-alloca/89714)
- [Closed PR of prior fix attempt](https://github.com/llvm/llvm-project/pull/180551)

Attached is a slide deck version of this RFC:

[parallel\_alloca.pdf](https://discourse.llvm.org/uploads/short-url/l9nKWV9OlTVVyAP1rclLyv8Bpbh.pdf) (114.6 KB)

---

<div class="post-metadata">

**Author:** ![efriedma-quic](https://sea1.discourse-cdn.com/flex021/user_avatar/discourse.llvm.org/efriedma-quic/32/2053_2.png) [@efriedma-quic](https://discourse.llvm.org/u/efriedma-quic)\
**Post date:** [March 9, 2026, 6:17pm UTC](https://discourse.llvm.org/t/rfc-llvm-parallel-alloca-intrinsic-for-vectorizable-stack-allocations/90106/2 "2026-03-09T18:17:30Z")

</div>

When is the memory allocated by llvm.parallel\_alloca freed?

(See also [incorrect transformation around icmp: unclear semantics of "lifetime" intrinsics leads to miscompilation · Issue #45725 · llvm/llvm-project · GitHub](https://github.com/llvm/llvm-project/issues/45725#issuecomment-1537597333) )

---

<div class="post-metadata">

**Author:** ![juliusikkala](https://sea1.discourse-cdn.com/flex021/user_avatar/discourse.llvm.org/juliusikkala/32/26596_2.png) [@juliusikkala](https://discourse.llvm.org/u/juliusikkala)\
**Post date:** [March 10, 2026, 12:57pm UTC](https://discourse.llvm.org/t/rfc-llvm-parallel-alloca-intrinsic-for-vectorizable-stack-allocations/90106/3 "2026-03-10T12:57:44Z")

</div>

Thanks for linking that thread, it somewhat changed my outlook on what to do with lifetimes here.

It seems that specifying such a `parallel_alloca_free` requires more effort than I have capacity for right now, and we’re mostly concerned with fixing this miscompilation with parallel loops that also impacts [pocl](https://github.com/pocl/pocl) and [prevents Slang’s LLVM target from using parallel metadata](https://github.com/shader-slang/slang/pull/9753#issuecomment-3841216995). So for now, memory would only be freed when the surrounding function returns.

Based on that discussion, I think we’ll want to make sure that we don’t tie too closely to the `llvm.lifetime.*` intrinsics here so that there’s enough wiggle room to add something like `parallel_alloca_free` in a separate RFC later and turn this `parallel_alloca` into a more general “alloca but fixed” intrinsic. I was planning on leaving lifetime intrinsics out anyway, but mostly to reduce the impact radius of this RFC. (will edit original post to mention intentional avoidance of lifetime intrinsics)

---

<div class="post-metadata">

**Author:** ![efriedma-quic](https://sea1.discourse-cdn.com/flex021/user_avatar/discourse.llvm.org/efriedma-quic/32/2053_2.png) [@efriedma-quic](https://discourse.llvm.org/u/efriedma-quic)\
**Post date:** [March 10, 2026, 5:32pm UTC](https://discourse.llvm.org/t/rfc-llvm-parallel-alloca-intrinsic-for-vectorizable-stack-allocations/90106/4 "2026-03-10T17:32:18Z")

</div>

“When the function returns” is problematic: you have N allocations, where N is the number of loop iterations, and you have to keep them all around until the function returns. So you need dynamic allocation, which is probably not what you want.

What you actually want is “when the function returns or the `@llvm.parallel_alloca` call runs again”? That’s tricky to specify, though, because it depends on the identity of the alloca, but instructions don’t have an identity: they can be cloned/merged/etc.

I guess you can add an `@llvm.parallel_alloca_anchor` intrinsic in the entry block, which returns a token, and use that token as an argument to `@llvm.parallel_alloca`.

---

<div class="post-metadata">

**Author:** ![juliusikkala](https://sea1.discourse-cdn.com/flex021/user_avatar/discourse.llvm.org/juliusikkala/32/26596_2.png) [@juliusikkala](https://discourse.llvm.org/u/juliusikkala)\
**Post date:** [March 12, 2026, 9:33am UTC](https://discourse.llvm.org/t/rfc-llvm-parallel-alloca-intrinsic-for-vectorizable-stack-allocations/90106/5 "2026-03-12T09:33:53Z")

</div>

> [@efriedma-quic](#):
>
> “When the function returns” is problematic: you have N allocations, where N is the number of loop iterations, and you have to keep them all around until the function returns. So you need dynamic allocation, which is probably not what you want.

Yeah, that’s not what I want. So far, I’ve mostly just reasoned about this by how it lowers: in the end, each call simply generates an `alloca` in the entry block.

> [@efriedma-quic](#):
>
> What you actually want is “when the function returns or the `@llvm.parallel_alloca` call runs again”? That’s tricky to specify, though, because it depends on the identity of the alloca, but instructions don’t have an identity: they can be cloned/merged/etc.

Yes, that is the behavior I was expecting indeed. Thanks, I just realized that this does indeed lead into a practical case of allocating a bit too much: when used in a vectorizable loop, the call gets cloned into both the vector body and the scalar epilogue, so the allocation occurs twice. Also, unrolling a loop with `@llvm.parallel_alloca` could be quite a tragedy.

> [@efriedma-quic](#):
>
> I guess you can add an `@llvm.parallel_alloca_anchor` intrinsic in the entry block, which returns a token, and use that token as an argument to `@llvm.parallel_alloca`.

All of this gets even more annoying when some of the calls to the “same” `@llvm.parallel_alloca` get vectorized and some don’t, because they lower differently. I suppose when lowering, one would need to collect all calls with the same token and find the widest one.

~~At that point it would seem cleaner to consider the `parallel_alloca_free` style approach, although I’m not fully sure how to proceed with that. I’ll have to take some more time thinking that through, because I’d love to avoid having to do any major overhauls to the stack allocation infrastructure in LLVM.~~

Edit: Nevermind, I’ll just try the token approach and see if it causes any trouble. If it’s fine, I’ll update the RFC to add that.
