[LoopInterchange] Some questions and a potential missing case?

Hi, I have some questions on Loop Interchange and what I think maybe a missing case?

I was observing this benchmark comparing llvm/clang to gcc on the BPi-F3, an in-order cpu.

I surmised from the assembly that gcc was able to perform loop-interchange on this nested for loop, which causes a significant performance gain since it can avoid doing a load/store every iteration of the inner loop.

  for (k=0; k<1000; k++) {
    for (i = n-1; i >= 0; i--) {
      y[i] += x[i];
    }
  }

Full code here: llvm-test-suite/SingleSource/Benchmarks/Shootout/ary3.c at main · llvm/llvm-test-suite · GitHub

I understand that loop-interchange isn’t enabled by default, but I wanted to try running it on the code for curiosity’s sake.

I executed this command to get the LLVM IR prior to the loop-vectorizer pass

../bin/clang --target=riscv64-linux-gnu \
    -march=rva22u64_v -mcpu=spacemit-x60 -O3 \
    --sysroot=/usr/riscv64-linux-gnu \
    -mllvm -print-before=loop-vectorize \
    -mllvm -print-module-scope \
    -S -emit-llvm ary3.c -o ary3_prevec.ll

and then this command to get the debug output from loop-interchange and DependenceAnalysis

../bin/opt -passes='loop-interchange' \
    -mtriple=riscv64 -mattr=+v \
    -debug-only=loop-interchange,da \
    -S ary3_prevec.ll -o ary3_interchange.ll 2> interchange_log.txt

I gleaned from the debug message was that the LoopInterchange pass made this dependency matrix: * =

Dependency matrix before interchange:
* =
... 
Failed interchange InnerLoopId = 1 and OuterLoopId = 0 due to dependence
Cannot prove legality, not interchanging loops 'for.cond11.preheader.us' and 'for.body14.us'

I trace this to the code where the dependency matrix is populated populateDependencyMatrix. My takeaway is that the outter loop is categorized as DVEntry::LE, so the direction is treated as ’ * '?

        for (unsigned II = 1; II <= Levels; ++II) {
          // `DVEntry::LE` is converted to `*`. This is because `LE` means `<`
          // or `=`, for which we don't have an equivalent representation, so
          // that the conservative approximation is necessary. The same goes for
          // `DVEntry::GE`.
          // TODO: Use of fine-grained expressions allows for more accurate
          // analysis.
          unsigned Dir = D->getDirection(II);
          if (Dir == Dependence::DVEntry::LT)
            Direction = '<';
          else if (Dir == Dependence::DVEntry::GT)
            Direction = '>';
          else if (Dir == Dependence::DVEntry::EQ)
            Direction = '=';
          else
            Direction = '*';
          Dep.push_back(Direction);
        }

I also noticed that to check for legality, function isLexicographicallyPositive is called, which I believe would return false since the first Direction is an ’ * ’ instead of ‘<’. This would subsequently cause isLegalToInterChangeLoops to return false.

static std::optional<bool>
isLexicographicallyPositive(ArrayRef<char> DV, unsigned Begin, unsigned End) {
  for (unsigned char Direction : DV.slice(Begin, End - Begin)) {
    if (Direction == '<')
      return true;
    if (Direction == '>' || Direction == '*')
      return false;
  }
  return std::nullopt;
}

Questions

  • Is this case a known limitation of the current LoopInterchange pass?
  • In the dependency matrix, I’m quite lost as to why the outer loop would be categorized as a DVEntry::LE, instead of DVEntry::LT.
  • How trivial of a fix would this be? My very naive assumption is that it wouldn’t be too difficult to tell this pass is legal in this case since the outer induction variable k is not used in the inner loop.

Apologies if this was too verbose. If these questions are too broad, I’d appreciate any links to existing documentation or RFCs I should read. Thanks for your time!

Firstly, you can invoke loop-interchange directly from clang with -floop-interchange. You should be able to pass -mllvm -debug-only=loop-interchange,da as well, if necessary.

I’ve not checked the details, but at a glance, I think the dependency direction for the outer loop should be DVEntry::ALL. The direction represents the possible relationships between the values of k when the same memory location is accessed. More precisely, in this case, it answers this question: when regarding the access to y as a function f(k, i), what are the possible relations between k0 and k1 when f(k0, i) = f(k1, i) for some i?

Here, f(k, i) = i. So, for example, f(0, 0) = f(1, 0) = ... = f(999, 0). Thus the outer loop has a dependency with all directions.

It’s easy to ensure that the induction variable is not used for memory access. The Dependence class has a Scalar field, so just checking that is sufficient. However, it’s not trivial what the scalar dependence means in the context of loop-interchange. In fact, there were several correctness issues due to the mishandling of scalar dependence in the past, e.g., [LoopInterchange] incorrect handling of scalar dependencies and dependence vectors starting with ">" · Issue #54176 · llvm/llvm-project · GitHub. So I don’t think it’s a good approach to somehow handle scalar dependence in order to apply loop-interchange in your case.

I believe the right direction is to allow swapping the adjacent loops if the direction vector is * = or = *. But this is also non-trivial in terms of soundness, and we need to think this through a bit more…

1 Like

I think I understand now, thank you for the thorough explanation! :grinning_face_with_smiling_eyes: