Replies: 42 comments 72 replies
aw_spawn_fork contains 2 things which are non-movable:
Since the child tasks begin executing immediately and capture a pointer to this control block (this happens during the call to fork()), its location must be pinned before they begin execution. There are several possible workarounds using detached tasks:
The need for these workarounds is a weakness of the current API that will be rectified in the future by #62 , which will enable std::future-like behaviors for tasks. Another option would be #75. As long as you don't use any blocking waits (tmc::post_waitable, std::future, std::mutex), all of the work will be executed eventually, even on a single core machine. |
In my benchmarks the performance is similar. One major difference is that An additional reason to use |
Threads don't suspend on locks in TMC. Only tasks. If you have an
|
This is one of the core issues with stackless coroutines - that they introduce the "function coloring problem". If you want to call You could call |
Yes... like you said, on some systems |
|
OMG, thanks a lot for such detailed explanations! I'll keep the issue opened for a while in case I have more quesitons, ok? |
It's not THAT bad in the engine I work on (this piece was not written by me) but still is a synchronous while-yield-blah waste of resources. I haven't found ANY DirectX API that would allow to do that asynchronously (post a request and get notified when the data is ready). Ideally we could suspend the rendering coroutine here and resume it once notified. What would be the best approach for this? I don't want to do sync-over-async crap. Something like, move such pieces to the main/legacy thread, suspend the rendering coro once it hits the point where we need to get the result, then wait for this loop in the main thread and post a coro resuming the rendering one?
Wouldn't this generate more code / yield worse optimization than a regular sync call to an inline which returns 2? (I use Clang 21 with
I suspect it would be more efficient to use the second approach? |
If the API requires you to poll periodically, you will need to use a timer. Some coroutine libraries offer async timer facilities, but TMC does not offer them directly - rather you can use them via the Asio integration in tmc-asio. There are a couple examples of using the asio timer facilities here: https://github.com/tzcnt/tmc-examples/blob/main/examples/asio/timer_mem_bench.cpp However it's worth noting that there is no truly async timer. Under the hood, a thread is blocking on the OS timer syscall, and then posting the results back to the executor queue when ready. Your approach of doing this manually using the main thread is equivalent. I like the SwitchToThread() call - this may actually help performance in some cases as it's more lightweight than actually blocking on the timer. |
Compilers are supposed to be able to inline coroutines, but often fail to do so at this time. The Clang 20 attributes (#61) are supposed to help with this. This particular item is near the top of my priority list... so it will be coming soon(tm). Notably you don't need to use inline tmc::task<int> fn() { co_return 2; }
tmc::task<void> fn2()
{
...
int x = co_await fn();
...
} |
I'm not sure about this. It depends on the implementation of |
In LLVM libc++, which I use, I'm a Linux dev, too, but this game is Windows-exclusive unfortunately :D (and porting its render from Dx 11.2 to Vulkan would be hell to me) |
|
|
The advantage of the synchronous unlock are if the unlocking task is latency sensitive and you don't want it to suspend, or if you want to make use of the RAII lock scope object. |
|
I'm going to convert this to a discussion; feel free to continue with any further questions there. |
Let's say we have the following: "Delegate"-like things (e.g. C++26's https://github.com/solbjorn/callme/blob/master/Impl/CallMe.h#L413 (all the implementations I'm aware of: LLVM, Folly, Abseil etc., do the same) I'm actually planning to add support for coroutines to my fork of CallMe and I suspect (haven't tried TBH, maybe it really is just that simple) that such invokers would not work as-is: as invokers are just regular functions. If invokers for coroutines must differ from regular ones, would something like this be enough? Some side info:
https://github.com/ConorWilliams/libfork/blob/main/include/libfork/algorithm/for_each.hpp#L60 (in his tests he passes both but it's wrapped with
Lots of stuff in the engine is implemented as a vector/deque of such "delegates". When the main loop reaches the point where they should be executed, it iterates over the container and calls them. Since the outer functions which handle these containers will be most likely converted to coroutines, it would be better to run each delegate as a coroutine as well to keep the function coloring etc etc. (but maybe you have a better idea how to organize this, since this code processing queues of delegates originates from 2005 and is not mine obviously; I only switched it from some ancient implementation to C++20's CallMe) |
|
Breh, I completely forgot about the utility awaitables and was wrapping oneliners in lambdas... When I need to execute something on the standalone co_await tmc::spawn([](auto& last) -> tmc::task<void> {
PIX_EVENT(DEFER_FLUSH_OCCLUSION);
for (auto light : last)
{
if (light == nullptr)
continue;
for (auto& svis : light->svis)
svis.flushoccq();
}
last.clear();
co_return;
}(Lights_LastFrame)).run_on(xr::tmc_cpu_st_executor());Now I'm thinking of just {
auto scope = co_await tmc::enter(xr::tmc_cpu_st_executor());
PIX_EVENT(DEFER_FLUSH_OCCLUSION);
for (auto light : Lights_LastFrame)
{
if (light == nullptr)
continue;
for (auto& svis : light->svis)
svis.flushoccq();
}
Lights_LastFrame.clear();
co_await scope.exit();
}The second should be more optimal I guess? IIRC P. S. In case you haven't seen my comment in #175:
:3 It's funny nonetheless that TMC is 3x faster than TF even without Chase-Lev queues :D |
|
I've found at least one such pattern in the code: m_playing_sounds.erase(std::remove_if(m_playing_sounds.begin(), m_playing_sounds.end(), CInappropriateSoundPredicate(sound_mask)), m_playing_sounds.end());where I need to change the For now it looks like I need to convert this to a manual loop in order to do that, there is no other way? |
|
Re "why I convert so many functions to coroutines" (note to myself mostly). For sure, I could just leave everything as it is, just use Lots of folks kept telling me that it's impossible to replace Luabind with Sol in the engine. It took me half a year and several thousand locs, but I made it. So I'm pretty sure this challenge is doable as well, especially given that I receive so huge and helpful support from you. (the same folks told me it's not possible to switch the engine from MSVC to Clang/clang-cl due to the legacy code that is too broken to be fixed -- lol that was EZ honestly, even though I wasn't good in C++ back then) BTW I'm curious how Tracy will work after the engine is coroutine-based -- I haven't read its code deeply, but my impression was that Tracy expects every function to start and end on the same thread and doesn't expect that a function can suspend. |
|
Just curious: tmc::post(
tmc::cpu_executor(),
tmc::detail::client_main_awaiter(
static_cast<tmc::task<int>&&>(ClientMainTask), &exitCode
),
0, 0
);Why is the root coroutine run with thread hint == 0 in |
|
Since it looks like you've finished the initial pass of the migration, I'd love to try playing the game. I was able to get your branch to build in Release, but no luck in Debug. However I'm not able to run it - the splash screen only pops up for a second and then disappears. When running in the debugger I see it's unable to find fsgame.ltx. However it doesn't emit any logs so I'm not sure what the next step is. I tried moving the files around and passing -fsgame parameter but no luck. I'm using the Steam 1.006 (?) version and was able to run the upstream OSGR engine just fine using the -steam parameter. Would you be willing to take some time to help me debug this? I joined the OGSR Engine and Open XRay discords so you can contact me there. If not I would appreciate some tips on getting it to find the fsgame / generate meaningful logs. Also is there a specific mod / graphical overhaul that I should be using? |
|
I've seen you're planning to introduce an option to make TMC fully header-only, could you maybe make it a tri-state, where the third option would make it header-only, but leave the hwloc-related functionality in ipp which I'd need to define in a .cpp file? I wouldn't probably be so critical against it if hwloc headers didn't include this horrendous windows.h. I'm planning to get rid of including it project-wide in future (vanilla engine code legacy), but give a chance to header-only TMC. As project-wide windows.h is a very bad idea. |
|
I know it's not your problem and I (and other users) should be more careful, so not asking for any changes, just curious... Is When I refactor something like void process_events()
{
// ...
}
// to
tmc::task<void> process_events()
{
// ...
}then the compiler will notify me if I missed a But when changing bool net_spawn()
{
// ...
}
// to
tmc::task<bool> net_spawn()
{
// ...
}
// later
if (!net_spawn())
//then unfortunately the compiler is not able to catch a missing I shot myself in the foot a couple times already, fortunately it was easy to find and fix. |
|
Hey, I've noticed the development of TMC has slowed down a bunch lately. Just wanted to make sure you are fine, no burnouts, no motivation loss, no problems in real life etc. Take care! (also seems like libfork has finally woken up, I'm curious what the new version will offer, although I'll stay with TMC with no doubts) |
|
Just noticed the runtime-benchmarks repo update... Man, WTH is this citor, how comes it's the fastest one? If I got it correctly, it's not even based on coroutines, right? I'm not planning to migrate from TMC obviously, just curious. Also interesting if anything from citor could serve as inspiration for improvements in TMC. |
|
I noticed that rwlock is in progress -- nice! New Screen Space Shaders will be released soon, plus I need a resync with the upstream OGSR after merging the shaders, but after it's done, I'd go forth with making the engine completely lockless (currently there are still several "regular" locks in regular functions -- I've been waiting for rwlock to convert them the most optimal way right away). |
|
What is CRL by the way? Anything useful there comparing to bare TMC? |
|
Speaking of do-and-complete-task: is there a way to implement stuff like const auto do_on_exit = tmc::finally([] -> tmc::task<void> { co_await coro(); });
...
co_return; // do_on_exit is called here |
|
Just curious - would you have any use for coroutine functionality inside Lua? If so, what use cases? |
|
I've noticed you forked LLVM and created a couple branches. Any interesting upcoming optimizations/PRs? BTW I was on the Netdevconf 0x1A and a couple folks from different organizations complained that their concurrency or coroutine libraries aren't NUMA-aware or slow or whatever, so I recommended them yours ^.^ |
|
I caught a rare crash inside my If that's the case, maybe it would be a good idea to mention in the documentation that they're not thread-safe and the user must serialize them manually if needed. I guarded the vector with a |
Uh oh!
There was an error while loading. Please reload this page.
Can I ask a few silly questions since I'm new to coroutines, but eager to switch my oneTBB-based engine to TMC?
aw_spawn_forkin a global struct and co_await it later from some other coroutine, not the one that spawned it?(my assumption comes from that
aw_spawn_forkdoesn't have a default constructor, so putting it in a struct is tricky. Moreover, you mentioned in the docs that they contain pointers tothis, which I got at "you must alwaysco_awaitthe results offork()within the coroutine that spawned them").What would be the best way for the following scenario:
Currently, I run two
task_groups from the former and them wait for them from the latter.task_groups lay in a shared struct, I don't pass pointers for them around the code.I'd like to not use
post_awaitable()and them block on the futures later, since that's not what coroutines are about.I thought of something like:
detach()them immediatelyco_awaitthe condvar/barrier to suspend instead of blocking in case they are not ready yet.But will this guarantee that the coroutines which I'm waiting for will execute for sure even if someone has 1 core and everything executes serially there?
And this doesn't look like an intended/obvious way...
Since
aw_mutexandex_braiddo nearly the same stuff, which one is faster from your code PoV? Mutex seems to be a bit heavier since it builds an awaiters list and need to repost every awaiter on each unlock? But if the contention is really narrow and it's unlikely for this mutex to be blocked,ex_braidcan incur more overhead?aw_mutexhasunlock()andco_unlock(). The latter can do sync transfer. Can sync transfer lead to that let's say 8 threads suspended on the same lock, but, if usingco_unlock(), these 8 coroutines will continue execution on only one thread serially even after this mutex' scope?If I have the following tree of function calls:
Does it mean that if I want to run a coroutine from
f3(), I need to callpost*()from it, even though it's still run onex_cpualready, but doesn't have a suspension point? Or, the preferred alternative would be to convert each of those functions to a coroutine?My main idea is to not block any of the coroutines I want to introduce with serial stuff like generic mutexes/futures/etc, so that only one thread (which runs main synchronous code) could be blocked at a time.
std::hardware_destructive_interference_sizeinstead of hardcoding to 64, it's a constexpr IIRC.But I've also seen that some developers started multiplying it by two since modern CPUs (at least x86_64) often tend to fetch 2 CLs at a time instead of one which could still provoke false-sharing.
Anyway, only benchmarking could give a reliable answer here.
All reactions