[PATCH] D55966: Ensure coro split pass only spills variables dominated by CoroBegin

Brian Gesiak via Phabricator via llvm-commits llvm-commits at lists.llvm.org
Fri Dec 21 14:33:27 PST 2018


modocache requested changes to this revision.
modocache added a comment.
This revision now requires changes to proceed.

Excellent progress! Thanks for working on this.

However I don't think this logic isn't quite right. My understanding is that in the context of coroutines transforms, the term "spill" refers to the definition of a value appearing before a suspend point, and a use of that value appearing after. All coroutines include an implicit suspend point at their start and end: the `promise_type.initial_suspend` and `.final_suspend` points. So any argument passed into a coroutine and then used from within its body does in fact "spill". For example:

  task<int> foo(int x) {
    // Implicit initial suspend point here.
    int y = x + 1; // The definition of 'x' comes at the start of the function, and its use is here past the implicit initial suspend point, so 'x' "spills" across a suspend point.
    // ...
  }

Because `x` spills, the coroutine transform passes move it onto the coroutine frame. This ensures that the value of `x` is preserved across suspend points.

The logic in this patch, however, prevents `x` in the example above from being considered a "spill." As a result, it is not moved onto the coroutine frame, and so its value is not preserved.

You can see this in practice by looking at the sample program in https://godbolt.org/z/YNX4dS, which I'll also paste below:

  #include <experimental/coroutine>
  #include <iostream>
  
  struct task {
    class promise_type;
    using handle_type = std::experimental::coroutine_handle<promise_type>;
  
    struct promise_type {
      task get_return_object() { return task{handle_type::from_promise(*this)}; }
      std::experimental::suspend_always initial_suspend() { return {}; }
      std::experimental::suspend_always final_suspend() { return {}; }
      void return_void() {}
      void unhandled_exception() { std::exit(1); }
  
      void *operator new(size_t sz, int x) {
        std::cout << __PRETTY_FUNCTION__ << " -> " << x << std::endl;
        return malloc(sz);
      }
    };
  
    handle_type handle = nullptr;
  
    explicit task(handle_type handle) : handle(handle) {}
    task(task&& t) : handle(t.handle) {}
    ~task() { if (handle) handle.destroy(); }
    void resume() { handle.resume(); }
  };
  
  task foo(int x) {
    std::cout << __PRETTY_FUNCTION__ << " -> " << x << std::endl;
    co_return;
  }
  
  int main() {
    task t = foo(42);
    t.resume();
  }

At `-O0` the program compilation succeeds, and when run it produces this output:

  static void *task::promise_type::operator new(size_t, int) -> 42
  task foo(int) -> 42

At `-O1` the program produces the error described in https://bugs.llvm.org/show_bug.cgi?id=36578. With your patch applied, it no longer triggers the LLVM coroutines assert, but the value of `x` is lost:

  static void *task::promise_type::operator new(size_t, int) -> 42
  task foo(int) -> 0

So while this patch avoids the assert, it doesn't fix the underlying issue: that `x` spills and so needs to be stored on the coroutine frame, but it cannot be moved onto the coroutine frame because it is *also* used in the allocator of the frame. I'm not sure what the ideal solution here is... perhaps `x` needs to be duplicated as `x.allocarg` -- `x.allocarg` would be passed into the allocator and would not spill, whereas `x` would continue to be used in the body of the function and would be spilled (and could be moved past and dominated by `@llvm.coro.begin`). But I am not certain this would work in practice -- maybe @GorNishanov has thought about this problem some more since we last spoke?



================
Comment at: lib/Transforms/Coroutines/CoroFrame.cpp:870
+  for (User *U : V.users())
+    if (cast<Instruction>(U) && !DT.dominates(&CBInst, cast<Instruction>(U)))
+      return false;
----------------
I believe `llvm::cast<T>` asserts if the cast to `T` fails, so this would assert for users that are not `Instruction`, such as `Constant`. In the future I'd recommend using `dyn_cast`, since it returns a false-y value if the cast fails:

```
if (auto *I = dyn_cast<Instruction>(U)) {
  if (!DT.dominates(..., I) {
    ...
  }
}
```

Writing it this way would also remove the need for the second `cast`.


Repository:
  rL LLVM

CHANGES SINCE LAST ACTION
  https://reviews.llvm.org/D55966/new/

https://reviews.llvm.org/D55966





More information about the llvm-commits mailing list