result: more performance notes

This commit is contained in:
Jacek Sieka 2020-04-06 14:05:56 +02:00 committed by zah
parent 4d20b25c02
commit 55c2ec8977
1 changed files with 58 additions and 16 deletions

View File

@ -66,8 +66,8 @@ type
##
## # Potential benefits:
##
## * Handling errors becomes explicit and mandatory - goodbye "out of sight,
## out of mind"
## * Handling errors becomes explicit and mandatory at the call site -
## goodbye "out of sight, out of mind"
## * Errors are a visible part of the API - when they change, so must the
## calling code and compiler will point this out - nice!
## * Errors are a visible part of the API - your fellow programmer is
@ -86,7 +86,7 @@ type
##
## * Handling errors becomes explicit and mandatory - if you'd rather ignore
## them or just pass them to some catch-all, this is noise
## * When composing operations, value must be lifted before funcessing,
## * When composing operations, value must be lifted before processing,
## adding potential verbosity / noise (fancy macro, anyone?)
## * There's no call stack captured by default (see also `catch` and
## `capture`)
@ -173,36 +173,78 @@ type
##
## # Performance considerations
##
## Compared to a simple return type, returning a Result sometimes incurs
## an overhead. Compared to raising an exception, this overhead is very low.
## Compared to returning a plain value, it may be significant, specially for
## large value types (deeply nested `object`:s) and value-like types (large
## `seq`:s):
## When returning a Result instead of a simple value, there are a few things
## to take into consideration - in general, we are returning more
## information directly to the caller which has an associated cost.
##
## Result is a value type, thus its performance characteristics
## generally follow the performance of copying the value or error that
## it stores. `Result` would benefit greatly from "move" support in the
## language.
##
## In many cases, these performance costs are negligeable, but nonetheless
## they are important to be aware of, to structure your code in an efficient
## manner:
##
## * Memory overhead
## Result is stored in memory as a union with a `bool` discriminator -
## alignment makes it somewhat tricky to give an exact size, but in
## general, `Result[int, int]` will take up `2*sizeof(int)` bytes:
## 1 `int` for the discriminator and padding, 1 `int` for either the value
## or the error. The additional size means that returning may take up more
## registers or spill onto the stack.
## * Loss of RVO
## Nim does return-value-optimization by rewriting `proc f(): X` into
## `proc f(result: var X)` - in an expression like `let x = f()`, this
## allows it to avoid a copy from the "temporary" return value to `x` -
## when using Result, this copy currently happens always because you need
## to fetch the value from the Result in a second step: `let x = f().value`
## To solve this, "move" support in the compiler is needed - it would
## allow moving the value out of the temporary result created here.
## * Extra copies
## To avoid spurious evaluation of expressions in templates, we use a
## temporary variable sometimes - this means an unnecessary copy for some
## types.
## * Bad codegen
## When doing RVO, Nim generates poor and slow code: it uses a construct
## called `genericReset` that will zero-initialize a value using dynamic
## RTTI - a process that the C compiler subsequently is unable to
## optimize. This applies to all types, and could be fixed in compiler.
## optimize. This applies to all types, but is exacerbated with Result
## because of its bigger footprint - this should be fixed in compiler.
## * Double zero-initialization bug
## Nim has an initialization bug that causes additional poor performance:
## `var x = f()` will be expanded into `var x; zeroInit(x); f(x)` where
## `f(x)` will call the slow `genericReset` and zero-init `x` again,
## unnecessarily.
## * Extra local copy in templates
## To avoid spurious evaluation of expressions in templates, we use a
## temporary variable sometimes - this means an unnecessary copy for some
## types.
##
## Relevant nim bugs:
## Comparing `Result` performance to exceptions in Nim is difficult - the
## specific performance will depend on the error type used, the frequency
## at which exceptions happen, the amount of error handling code in the
## application and the compiler and backend used.
##
## * the default C backend in nim uses `setjmp` for exception handling -
## the relative performance of the happy path will depend on the structure
## of the code: how many exception handlers there are, how much unwinding
## happens. `setjmp` works by taking a snapshot of the full CPU state and
## saving it in memory when enterting a try block (or an implict try
## block, such as is introduced with `defer` and similar constructs) which
## is an expensive operation.
## * an efficient exception handling mechanism (like the C++ backend or
## `nlvm`) will usually have a lower cost on the happy path because the
## value can be returned more efficiently. However, there is still a code
## and data size increase depending on the specific situation, as well as
## loss of optimization opportunities to consider.
## * raising an exception is usually (a lot) slower than returning an error
## through a Result - at raise time, capturing a call stack and allocating
## memory for the Exception is expensive, so the performance difference
## comes down to the complexity of the error type used.
## * checking for errors with Result is local branching operation that
## happens on the happy path - even if all that is done is passing the
## error to the next layer - when errors happen rarely, this may be a cost.
##
## An accurate summary might be that Exceptions are at its most efficient
## when errors are not handled and don't happen.
##
## # Relevant nim bugs
##
## https://github.com/nim-lang/Nim/issues/13799 - type issues
## https://github.com/nim-lang/Nim/issues/8745 - genericReset slow
## https://github.com/nim-lang/Nim/issues/13879 - double-zero-init slow