As promised, my article about Nim's new implementation strategy arrived: https://nim-lang.org/araq/gotobased_exceptions.html
You can discuss my results here in this forum thread.
type | Ubuntu GCC | Mac LLVM | Windows VC++ |
---|---|---|---|
C++ | 8043.0 | 6185.0 | 8908.2 |
goto | 8936.0 | 6336.2 | 8685.0 |
setjmp | 10214.4 | 7242.0 | 10492.6 |
Ubuntu GCC
Intel(R) Xeon(R) CPU @ 2.30GHz
gcc version 7.4.0
C++
RunningStat(
number of probes: 5
max: 8373.0
min: 7835.0
sum: 40215.0
mean: 8043.0
std deviation: 204.5238372415303
)
goto
RunningStat(
number of probes: 5
max: 8955.0
min: 8930.0
sum: 44680.0
mean: 8936.0
std deviation: 9.570788891204304
)
setjmp
RunningStat(
number of probes: 5
max: 10319.0
min: 10178.0
sum: 51072.0
mean: 10214.4
std deviation: 53.23758071137343
)
Mac LLVM
2.6 GHz Intel Core i7
Apple LLVM version 10.0.1
C++
RunningStat(
number of probes: 5
max: 6393.0
min: 6030.0
sum: 30925.0
mean: 6185.0
std deviation: 127.5476381592383
)
goto
RunningStat(
number of probes: 5
max: 6486.0
min: 6230.0
sum: 31681.0
mean: 6336.2
std deviation: 95.63764949014589
)
setjmp
RunningStat(
number of probes: 5
max: 7439.0
min: 6989.0
sum: 36210.0
mean: 7242.0
std deviation: 145.2391131892507
)
Windows VC++
Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz
Microsoft (R) C/C++ Optimizing Compiler Version 19.23.28107 for x64
C++
RunningStat(
number of probes: 5
max: 9667.0
min: 8447.0
sum: 44541.0
mean: 8908.200000000001
std deviation: 430.4074348800213
)
goto
RunningStat(
number of probes: 5
max: 9261.0
min: 8481.0
sum: 43425.0
mean: 8685.0
std deviation: 290.22542962325
)
setjmp
RunningStat(
number of probes: 5
max: 10625.0
min: 10419.0
sum: 52463.0
mean: 10492.6
std deviation: 73.94754897898915
)
following example is even more drastic, 3x speedup over C++ (in case where there are a lot of thrown exceptions)
import std / [times, monotimes, stats, strutils]
const depthRaise = 100
type MyExcept = object of CatchableError
proc fib(m: string, depth = 0): int =
if depth == depthRaise: raise newException(MyExcept, "gook")
let n = parseInt(m)
doAssert n > 0
if n <= 1: return 1
else: return fib($(n-1), depth + 1) + fib($(n-2), depth + 1)
var numCaught = 0
proc benchFun()=
try: echo fib($(depthRaise+1))
except MyExcept: numCaught.inc
when true:
var r: RunningStat
for iterations in 1..5:
let start = getMonoTime()
for i in 0 ..< 100000:
benchFun()
echo numCaught
r.push float((getMonoTime() - start).inMilliseconds)
echo r
In the "goto based exceptions" mode checked runtime errors like "Index out of bounds" or integer overflows are not catchable and terminate the process.
Hard to love the new exceptions if one cannot defensively protect against out of bounds errors and risk to see their process terminated... :-(
Better safe than sorry. One of the reasons why I'm using nim rather than C, for instance, is because it has safe defaults and I can decide to turn them off if needed. That's strange that the C standard has been adding safe bounds to the language while nim is removing them...
For instance, you've been training a neural networks for hours/days and then an out of bound/overflow exception occurs because of a bug in your code. Perhaps you would like to trap it in order of dumping the state of the neural network into a file and being able to resume the training when you've corrected the bug.
Or you could decide to use another algorithm when an overflow occurs.
That's strange that the C standard has been adding safe bounds to the language while nim is removing them...
New policy: Remarks so wrong that they are close to trolling will be removed.
I'm not sure what would be a reasonable way to handle an out of bounds exceptions or an overflow even if it were catchable. It's not like a file not found where we can expect user interaction behind.
For an HTTP server, output an error 500 and go on processing other requests (the point of Araq still stands).
For anything with user interaction: would you prefer a calculator to display "number too large" or rather crash when you enter "2^2^500"?
I really don't see why crashing can be an acceptable way to deal with errors
Like @b3liever said you should always validate your user inputs because you should expect them to be wrong.
When you get an out-of-bounds or an exception, your program is in inconsistent state. Doing anything further has the heavy potential to do undefined things and potentially corrupt state even further.
Crashing is a security measure to avoid propagating application corruption eventually down to a database or any storage. Not crashing is unsafe.
Also crashing is not about errors, it's about exceptions, something that is unexpected. If you can expect that an input will be wrong (because it's from "outside"), don't use exceptions, validate with error codepaths. That was the main idea between TaintedString.
Regarding numbers for example, one should always validate before dividing in case the input is 0, because that will bring down the application with a SIGFPE and this is emitted at the level below the application. This means if some exception kinds become more unforgiving (i.e. Overflow becomes DivByZero-like => no recovery), Nim should provide a way to validate those before committing to the operation (i.e. check overflow before)
How can I validate if computing 2^500 is safe without, you know, asking the computer to compute 2^500 and dealing with the overflow?
Is there some way to check the hardware capabilities? I'm sorry if this is a dumb question, but C (for instance) will tell you what the max integer is, and it ought to be easy to work out from that whether 2^500 is safe. I imagine Nim will do the same but it's been a while since I looked at this.
it ought to be easy to work out from that whether 2^500 is safe.
For simple cases, yes you can determine in advance when you overflow, even statically at compilation time. But even for simple functions like Ackermann function or the Hailstone sequence, it can be difficult determining when it overflows or not.
One of the reasons why the exception mechanism is important even for overflow is that it lets the programmer decide the locality of error management (in that case overflow). Returning a status code like in C library has show that programmers usually don't test results after each calls. And if they do, the resulting source is mostly errors processing... Implementing a global signal handler has often a too large scope and resuming is too difficult. Exceptions trapping has been successful letting the programmer decide the scope where they apply, being a single line or a whole program, and how to resume from their capture (depending on the language).
Finding in advance if an overflow will occur is the way to go when possible. But unfortunately, there are many occasions when it's not possible. And exceptions are the nicest error management system for the moment.
I think we are conflating two types of exceptions: recoverable ones, that can be safely catched and enable granular error management, and fatal ones that Nim cannot handle, where the only safe option is a restart.
I would argue that the second type should be exceedingly rare by default, due to their impact. Opt-in (with warning) for other options.