An asgiref async_to_sync microbenchmark

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.

1 Like