refactor: check endpoint handler is async only once (#2536)
* refactor: check endpoint handler is async only once
We improve the dispatch in the routing module to only check once whether the handler is async. This gives an improvement of 2.5% (sync), 1.82% (async) in the number of requests/s. The average latency decreased 1.6% (sync) and 1.5% (async).
Note that we had to use a cast in the helper function, as the typeguard does not work for the negative case. In the main branch the code is working without a cast, because the typeguard return type is in practice `AwaitableCAllable[Any]`, which end up swallowing the other types in the union.
Benchmarking
We use a simple json app, with both a sync and async endpoint, and the wrk tool to get the measurements.
The measuerements were done on a Macbook Pro with M1 chip, 16GB of memory and macOS 12.3. The Python version used for the tests is Python 3.12.2, and the uvicorn version is 0.27.1
Before
```
$ wrk http://localhost:8000/sync
Running 10s test @ http://localhost:8000/sync
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 733.77us 55.57us 3.21ms 78.35%
Req/Sec 6.84k 147.06 7.15k 87.13%
137474 requests in 10.10s, 18.35MB read
Requests/sec: 13610.69
Transfer/sec: 1.82MB
The app used for the test is as follows
```python
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
import uvicorn