Hi all!
I’ve been trying to use the llvm.loop.parallel_accesses metadata lately, but I’m confused by its LangRef documentation not seemingly matching with the actual implementation. Its interactions with local variables in Clang also seem unpredictable and unintuitive to me.
The langref says the following about llvm.loop.parallel_accesses:
It denotes that no loop-carried memory dependence exist between it and other instructions in the loop with this metadata.
Yet for some reason, LoopAccessAnalysis.cpp still checks for stores involving loop-invariant addresses in such loops and LoopVectorizationLegality.cpp looks for those regardless of whether the loop is parallel or not. Which sounds odd, because the metadata is supposed to make the iterations independent, so those things shouldn’t matter, right?
Now, the part where the semantics get unpredictable is when an alloca is involved. Let’s take the following C code as an example:
void func(int *A, int i) {
int t[32];
// Could also be an unrollable loop, but unrolled manually to show that
// this is not an outer-loop vectorization problem.
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;
t[16] = i; t[17] = i; t[18] = i; t[19] = i;
t[20] = i; t[21] = i; t[22] = i; t[23] = i;
t[24] = i; t[25] = i; t[26] = i; t[27] = i;
t[28] = i; t[29] = i; t[30] = i; t[31] = i;
A[i] = t[i];
}
void foo(int *A, int n) {
#pragma clang loop vectorize(assume_safety)
// or #pragma omp simd, both add `llvm.loop.parallel_accesses`.
for (int i = 0; i < n; i++) {
func(A, i);
}
}
Now, to the C programmer, it looks like there are no dependencies between loop iterations, as the t array is fully contained inside a function which is called once per iteration. However, in Clang, func gets inlined and this emits an alloca inst in the first basic block of foo, outside of the loop; the same alloca is shared for the t of every iteration. As the end result, this loop is actually not safe to vectorize after all, and writes incorrect values to A: 7 7 7 7 7 7 7 7 15 15 15 15 15 15 15 15 23 23 23 23 23 23 23 23 31 31 31 31 31 31 31 31 (on my znver1 CPU). I opened an issue about this a bit earlier.
It seems difficult to use this metadata properly without the programmer constantly checking IR in between, as small things like using local arrays can suddenly make it unsafe. Ideally, I think the alloca should get privatized so that each vector lane would have its own copy of t instead of accidentally sharing it like here. I think that could work by adding the same llvm.access.group MD to alloca insts that is already used for loads and stores - this would carry enough information to be able to implement privatization in the loop vectorizer.
It feels like we’re in an unfortunate situation where llvm.loop.parallel_accesses doesn’t do exactly what it says on the tin (being a bit more conservative than it says), but also simultaneously interacts unsafely with the way high-level languages may emit alloca outside of the loop.
So in short, I have the following questions:
- Are loops with
llvm.loop.parallel_accessesreally supposed to care about stores to loop-invariant addresses or is this just an oversight? - Is the above C program behaving as intended with
llvm.loop.parallel_accesses? - Is there any means to privatize
allocas when vectorizing in LLVM? If not, would the project be open to such a thing being contributed?