August 31st, 2021

C++20 Coroutine Improvements in Visual Studio 2019 version 16.11

Jonathan Emmett
Senior Software Engineer

This post includes contributions from Terry Mahaffey and Ramkumar Ramesh.

We last blogged about coroutine support in Visual Studio 2019 version 16.8. In the releases since 16.8 we’ve introduced several new coroutine features and improvements. This post is a round up of those improvements, all available in Visual Studio 2019 16.11.

Debugging Improvements

Since Visual Studio 2019 version 16.9, stepping into a coroutine call will now land directly in the coroutine body (unless it is set to initially suspend, in which case the step becomes a “step over”). Stepping over a co_await will land in the logical statement following co_await for the coroutine – which may be in a completely different execution context (even another thread)! This allows stepping through coroutines to seamlessly match the logical flow of the application and skip intermediate implementation details. For the best debugging experience, implementation details of the coroutine state should be marked as non-user code. Stepping through coroutines now also shows function parameters as expected in the Locals window so you can see the state of the application, similar to stepping through synchronous functions.

Inspecting the state of a suspended coroutine is now easier with some improvements to the debugging visualizers for standard coroutines. The legacy coroutine_handle visualizers could display special indicators for the initial and final suspend points, but only showed a number for other suspend points. This number was not always easy to map back to a particular point in the original coroutine. The visualizer also showed the name of the coroutine but only as a modified, internal name generated by the implementation with no signature information.

Image of legacy coroutine handle visualizer shows name as "sample_coroutine$_ResumeCoro$1(void)"
Legacy visualizer shows the name as “sample_coroutine$_ResumeCoro$1(void)”

With the new coroutine handle visualizer introduced in Visual Studio 2019 16.10 the function name is now correct and includes full signature information to help distinguish overloaded coroutines. The suspend point information for suspend points other than initial and final suspend also includes the source line number to make it easier to find.

Image of new coroutine handle visualizer with name "sample_coroutine(int)" and suspend point line number
New visualizer shows the name as “sample_coroutine(int)” and is suspended on line 35

/await:strict

The earlier blog post outlines some issues with legacy await mode and the rationale for keeping the /await switch distinct from C++20 coroutine support in /std:c++latest. Legacy mode is useful for users who were early adopters of C++ coroutines, but they are not standard coroutines. 

The/await switch predates not only our /std:c++latest and /std:c++20 switches but also /std:c++17. Early adopters were able to make use of coroutines long before they became part of the C++ standard. These users could use coroutines without requiring their code to be C++20 conformant or even necessarily C++17 conformant. With standard coroutines available only under C++20 and latest modes, early adopters of coroutines who cannot move their code to a more recent language version were stuck with the legacy implementation of coroutines under/await. They could not take advantage of some new features like symmetric transfer and improved debugger support, even if they were willing to make source changes to the coroutines themselves to bring them in line with the C++20 standard.
Starting in Visual Studio 2019 version 16.10 we introduced a new switch to help early coroutine adopters transition to conformant coroutines and use all features available in standard coroutines: /await:strict. Using this switch instead of /await enables the same C++20 coroutine support as standard mode but without all the other requirements of /std:c++20. This includes support for all standard C++20 coroutine features and debugger integration and disables all the legacy extensions still supported under /await. The only difference between /std:c++20 coroutines and /await:strict is the latter does not define the spaceship operator for std::coroutine_handle. Instead, it defines individual relational operators.
Migrating from /await to /await:strict may require source changes if your code relies on extensions that were not adopted into C++20. Like Standard mode it uses the <coroutine> header and the std namespace, so your code will be drop-in ready for C++20. Code compiled with /await:strict uses the same coroutine ABI as /std:c++latest, so coroutine objects are compatible between the two modes.
We encourage all users of /await to migrate to /await:strict. You can take advantage of all new coroutine features as well as ensure your coroutine code is ready for C++20 when you can move to a C++ language version that officially supports coroutines. We expect to deprecate and remove the  /await switch at some point in the future.

Stability Improvements

Visual Studio 2019 version 16.11 also includes several important fixes to improve the stability and reliability of coroutines.

The largest change relates to how the optimizer does what is called “promotion”, which is the algorithm to decide which variables get placed on the coroutine frame and which variables remain on the (traditional) stack. Many coroutine bugs can be traced back to an incorrect decision here. Typically this shows up as a crash, or as a variable having an incorrect or random value after a coroutine resumes execution. This promotion algorithm has been rewritten to be more accurate, and the result is less crashes and a much smaller coroutine frame size overall. The old algorithm is still accessible by passing /d2CoroNewPromotion- to cl.exe.

A related fix concerns how exception objects are stored. The lifetime rules for exceptions can get complicated, and they need to be handled specifically when it comes time to decide variable promotion.

A bug was found and fixed related to catch blocks in coroutines. Under certain circumstances (namely, when the only throwing call in a try block was from a user defined awaiter method) the optimizer could erroneously conclude a catch block was dead, and incorrectly remove it. The compiler is now aware that awaiter methods can throw.

Finally, a serious issue was resolved related to how and when destructors are invoked. This relates to how the construction state is tracked in coroutines for certain objects which are conditionally destroyed when leaving a scope. It comes up most when constructing objects when using the conditional (ternary) operator. The bug manifests itself by a destructor for such temporary objects not being invoked, or in certain cases invoked twice. This has also been fixed in 16.11.

Feedback

We urge you to try out C++ coroutines in Visual Studio, either with C++20 or now with /await:strict, to see how asynchronous functions can help make your code more natural. As always, we welcome feedback on our coroutine implementation either in the comments below, or for bug reports and feature requests directly on Developer Community.

Category
C++Coroutine

Author

Jonathan Emmett
Senior Software Engineer

Developer on the Visual C++ compiler front-end.

3 comments

Discussion is closed. Login to edit/delete existing comments.

  • Sylvester Hesp

    Great stuff! Though I’m particularly interested in heap allocation elision. Has there been made any progress since 16.8? I couldn’t get the compiler to avoid coroutine frame heap allocation with even the simplest of coroutines.

    • Terry MahaffeyMicrosoft employee

      Hey Sylvester! Short answer, no: performance improvements like fully productizing heap elision is on our backlog, but we have chosen to focus on stability and correctness improvements first. We’ll keep you updated!

      • Sylvester Hesp

        Understandable. I can’t wait to start using coroutines for my job system, but the pretty hefty alloc for each coroutine is currently holding me back. That being said, of course stability and correctness is even more important 😁.