Dicebot recently posted about the use of exceptions as flow control in vibe.d, and how bad it performs:
http://forum.dlang.org/thread/[email protected]?page=8#post-lqchrhwplxeitefecwne:40forum.dlang.org

Now I did some tests (very unscientific) with a simple app, testing three things:

1) Normal program flow without exceptions

import std.random;
void index(HTTPServerRequest req, HTTPServerResponse res)
{
    auto i = uniform(0, int.max);
    res.render!("index.dt", req, i);
}

The template just outputs the generated random number, nothing fancy.

2) Throwing a new exception each time

void index(HTTPServerRequest req, HTTPServerResponse res)
{
    const exc = new HTTPStatusException(HTTPStatus.NotFound);
    throw exc;
}

3) Creating the exception once and reusing it

void index(HTTPServerRequest req, HTTPServerResponse res)
{
static const exc = new HTTPStatusException(HTTPStatus.NotFound);
    throw exc;
}

Each of these variants I tested with two versions of vibe.d:

A) Unmodified vibe.d 0.7.17

B) vibe.d master with the following changes applied:

diff --git a/source/vibe/http/server.d b/source/vibe/http/server.d
index 2110cd2..7e29963 100644
--- a/source/vibe/http/server.d
+++ b/source/vibe/http/server.d
@@ -1395,10 +1395,11 @@ private bool handleRequest(Stream http_stream, TCPConnection tcp_connection, HTT
                // if no one has written anything, return 404
enforceHTTP(res.headerWritten, HTTPStatus.notFound);
        } catch (HTTPStatusException err) {
- logDebug("http error thrown: %s", err.toString().sanitize); - if (!res.headerWritten) errorOut(err.status, err.msg, err.toString(), err);
+
+// logDebug("http error thrown: %s", err.toString().sanitize); + if (!res.headerWritten) errorOut(err.status, err.msg, /*err.toString()*/"", err); else logDiagnostic("HTTPStatusException while writing the response: %s", err.msg); - logDebug("Exception while handling request %s %s: %s", req.method, req.requestURL, err.toString()); +// logDebug("Exception while handling request %s %s: %s", req.method, req.requestURL, err.toString()); if (!parsed || res.headerWritten || justifiesConnectionClose(err.status))
                        keep_alive = false;
        } catch (Throwable e) {

This just disables calling `toString()` on the exception to avoid creating a backtrace, which according to Adam Ruppe's PR is slow:
https://github.com/D-Programming-Language/druntime/pull/717

Then I compiled with:
dub run --build=release
and used ab2 to benchmark:
ab2 -n 10000 -c 1000 -k -r http://localhost:8080/

The results:

                           A) vibe.d 0.7.17      B) vibe.d patched
1) no exceptions                13000 req/s            13000 req/s
2) dynamic exceptions             200 req/s              800 req/s
3) static exceptions              320 req/s            19000 req/s

(The smaller numbers are subject to a lot of variance. Additionally, for B/3 I increased the number of requests to 100000 instead of 10000.)

Now, it can be clearly seen that not generating a backtrace results in 300% gain in throughput (A/2 vs. B/2). Also, reusing exceptions also gives us about 60% gain (A/2 vs. A/3). A/1 and B/1 are equal, which is to be expected.

But the big surprise is B/3. It seems as if there is no overhead at all! I thought this maybe comes from inlining, so I compiled with `--build=debug` which doesn't pass `-inline` to dmd, but it still served about 13000 req/s.

Does anyone have an idea what's going on here? Why the massive performance gain with B/3, when neither A/3 nor B/2 are so much faster? This indicates either a bug, or a really great opportunity for optimization.

In general: Is it even safe to reuse exceptions? Even assuming that caught exceptions arent't kept around, what would happen if the throwing function were called via different code paths? Would the stack trace be cached and kept around, and thus be potentially wrong? Does the language specification give any guarantees?

Reply via email to