c .= a .* b
e .= c .+ d
into e .= a .* b .+ d
Dot near operations means that they are applied elementwise without creating intermediate arrays. On CPU, Julia compiler / LLVM then generates code that reads and writes to memory exactly once (unlike e.g. what you would get with several separate operations on numpy arrays). On GPU, CUDAnative generates a single CUDA kernel which on my tests is ~1.5 times faster then several separate kernels. Note that `.=` also means that the result of operation is directly written to a (buffered) destination, so it no memory is allocated in the hot loop. from aiohttp import web
async def handle(request):
return web.Response(text="Hello")
app = web.Application()
app.add_routes([web.get('/', handle),])
web.run_app(app)
using `wrk` for testing: $ wrk -t1 -c1000 -d30s http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
1 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 164.59ms 17.12ms 537.35ms 83.97%
Req/Sec 6.05k 0.95k 7.74k 74.33%
180749 requests in 30.08s, 26.72MB read
Requests/sec: 6008.75
Transfer/sec: 0.89MB
So we have ~6k rps on a single CPU core. As far as I remember, Tornado has ~4k rps, while built-in Flask server can process only about ~1k requests per second . Yes, you are unlikely to use Flask dev server in production, but for aiohttp it's indeed a recommended way. using HTTP
HTTP.listen() do request::HTTP.Request
return HTTP.Response("Hello")
end
which gives: $ wrk -t1 -c1000 -d30s http://127.0.0.1:8081/
Running 30s test @ http://127.0.0.1:8081/
1 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 105.95ms 108.35ms 1.99s 98.40%
Req/Sec 9.65k 1.69k 13.66k 80.21%
271917 requests in 30.09s, 16.10MB read
Socket errors: connect 0, read 0, write 0, timeout 274
Requests/sec: 9035.73
Transfer/sec: 547.67KB
So it's 9k (with a few failed requests though). Z[i,j] = X[i,k] * Y[k,j] # implicitly sum over k
It worked pretty well and I was able to get results in Einstein notation for element-wise functions, matrix multiplication and even convolutions. y = sum(W * X + b)
if `W` is a matrix, then `dy/dW` is also a matrix (without sum it would be a 3D tensor). This is the reason why backpropogation algorithm (and symbolic/automatic differentiation in general) in machine learning works. So finally I ended up with a another library [3], which can only deal with scalar outputs, but is much more stable. In [2]: import numpy as np
In [3]: X = np.ones(1000000000, dtype=np.int)
In [4]: Y = np.ones(1000000000, dtype=np.int)
In [5]: %time X = X + 2.0 * Y
CPU times: user 10.4 s, sys: 27.1 s, total: 37.5 s
Wall time: 46 s
In [6]: %time X = X + 2 * Y
CPU times: user 8.66 s, sys: 26 s, total: 34.7 s
Wall time: 42.6 s
In [7]: %time X += 2 * Y
CPU times: user 8.58 s, sys: 23.2 s, total: 31.8 s
Wall time: 37.7 s
In [8]: %time np.add(X, Y, out=X); np.add(X, Y, out=X)
CPU times: user 11.3 s, sys: 25.6 s, total: 36.9 s
Wall time: 42.6 s
No surprise, Julia makes nearly the same result: julia> X = ones(Int, 1000000000);
julia> Y = ones(Int, 1000000000);
julia> @btime X .= X .+ 2Y
34.814 s (6 allocations: 7.45 GiB)
UPD. Just noticed 7.45Gib allocations. We can get rid of it as: julia> @btime X .= X .+ 2 .* Y
20.464 s (4 allocations: 96 bytes
or: julia> @btime X .+= 2 .* Y
20.098 s (4 allocations: 96 bytes) C .= A .+ B
Benchmarks for 3 matrices of size 1000x1000: julia> using BenchmarkTools
julia> @benchmark C = A + B
BenchmarkTools.Trial:
memory estimate: 7.63 MiB
allocs estimate: 2
--------------
minimum time: 2.359 ms (0.00% GC)
median time: 2.713 ms (0.00% GC)
mean time: 3.794 ms (28.81% GC)
maximum time: 62.708 ms (95.27% GC)
--------------
samples: 1314
evals/sample: 1
julia> @benchmark C .= A .+ B
BenchmarkTools.Trial:
memory estimate: 128 bytes
allocs estimate: 4
--------------
minimum time: 1.232 ms (0.00% GC)
median time: 1.320 ms (0.00% GC)
mean time: 1.356 ms (0.00% GC)
maximum time: 2.572 ms (0.00% GC)
--------------
samples: 3651
evals/sample: 1
Note that memory usage dropped from 7.63MiB to 128 bytes.
1) async programming vs. threading
2) infectious async/await syntax
Async programming is great. Coroutines are a powerful tool, both for expressing your ideas more clearly and for improving performance in IO-heavy systems.
async/await syntax may not be the best design for async programming though. Consider example in Julia:
`foo()` returns an asynchronous `Task`, `bar()` awaits this task, and you can invoke `bar()` from whatever context you want. Now look at the Python version with async/await keywords:
Oops, we can't make `bar()` synchronous, it MUST be `async` now, as well as all functions that invoke `bar()`. This is what is meant my "infectious" behavior.
Maybe we can wrap it into `asyncio.run()` then and stop the async avalance?
Yes, it works in synchronous context. But path to asynchronous context is now closed for us:
So in practice, whenever you change one of your functions to `async`, you have to change all its callers up the stack to also be `async`. And it hurts a lot.
Can we have asynchronous programming in Python without async/await. Well, prior to Python 3.5 we used generators, so it looks like at least techically it's possible.