So I previously posted about sync_to_async performance, roughly.
As part of exploring alternatives to my “unasyncifying codegen” DEP I realized I had made a very massive category error: I had somehow decided that async_to_sync
and sync_to_async
would exhibit symetric behavior.
But of course “wait for some single thread to do something” and “spin up an event loop to do something” are… well they’re very different.
So I wrote a microbenchmark to look at what we could consider to be the “cost” of async_to_sync
.
It trues to run the following coroutine in various scenarios:
async def operation(start_time):
end_time = time.time_ns()
metrics.append((start_time, end_time))
(metrics
is a global to hold onto the timings)
Check out the gist to see what the benchmarks are, but the short version is that basically it seems like the cost of async_to_sync
is quite low. On 100k iterations on my macbook pro M3, we’re looking at a 99th percentile under 1 second
An example run result (these numbers are in milliseconds), on 100k iterations:
name P10 P25 P50 P75 P95 P99 P99.9
0 bench1 0.055 0.056 0.059 0.061 0.072 0.082 1.317012
1 bench2 0.055 0.056 0.058 0.061 0.070 0.077 0.722020
2 bench3 0.030 0.031 0.031 0.032 0.036 0.040 0.254001
3 bench4 0.055 0.057 0.058 0.061 0.071 0.080 0.451000
4 bench5 0.055 0.056 0.058 0.061 0.071 0.078 0.219001
The results aren’t super stable, in that re-runs will sometimes be even faster, but these P50s (median costs) of around 0.06ms seem to be pretty stable.
To re-iterate, here we’re measuring the cost of the wrapping. If going from a sync implementation to async_to_sync(async_implementation)
, there’s going to be extra costs from using coroutines rather than direct calls for some steps IMO. But we could probably ignore async_to_sync
in the reasoning relative to the rest of it all (though we still need to benchmark stuff)
But at the very least I am pretty confident to say the following: if we were to switch out a sync implementation of something with an async implementation wrapped with async_to_sync
, the costs of async_to_sync
is very low relative to any sort of I/O. I don’t think it’s possible to use async_to_sync
in a getattr
magic, but in most ORM calls? Might be totally OK.
@carltongibson would appreciate a look at the gist, because maybe you’ll be aware of some ways of triggering more pathological behavior.