In C it would still have use in the implementation of orderly error handling -- the pattern where you hand-implement exception handling in C by putting an on_error: label at the end of the function that is goto'd on error. The addition of some orderly construct for this in C would eliminate that case, leaving no real role for goto there either.
In 20 years of using C/C++, I've found one use for "goto" that's hard to substitute: simulating coroutines that yield to an outer context. (Similar to C# yield return).
A "break label" gets you out of a loop. I needed to use "goto" to jump back into the middle of a loop to resume where the "coroutine" previously left off. The keywords "break/setjmp/longjmp" wouldn't have been substitutes for this particular use case.
Here's the base code without gotos:
typedef enum { ADD, MUL, ..., END } opcode;
void run() {
opcode ins;
while (1) {
ins = fetch_next_inst();
switch (ins) {
case ADD:
perform_addition();
break;
case MUL:
perform_multiplication();
break;
...
case END:
wrap_up();
return;
}
}
}
You have 3 jumps on each loop. From the break to the end of the loop, then from the end to the top, and one from the switch to the right case. The first one might be optimized away, but let's remove it explicitly. typedef enum { ADD, MUL, ..., END } opcode;
void run() {
opcode ins;
start:
ins = fetch_next_inst();
switch (ins) {
case ADD:
perform_addition();
goto start;
case MUL:
perform_multiplication();
goto start;
...
case END:
wrap_up();
return;
}
}
Assuming a non lousy compiler, we haven't improved anything yet. But now the fun starts. We can go down to one jump for each iteration. typedef enum { ADD, MUL, ..., END } opcode;
#define NEXT() \
do { \
ins = fetch_next_inst(); \
goto *jump_table[ins]; \
} while(0)
void run() {
opcode ins;
static void *jump_table[] = { &&add_l, &&mul_l, ..., &&end_l };
NEXT();
add_l:
perform_addition();
NEXT();
mul_l:
perform_multiplication();
NEXT();
...
end_l:
wrap_up();
return;
}
Voila! a single jump every time around. Now, depending on what kind of architecture you're running on, the size of the cache, etc, this may or may not be faster.Granted, this is not the kind of code you write everyday. But sometimes speed matters, and good luck writing this without gotos.
But assuming a sufficiently smart compiler when you depend on your code being fast tends to cause issues. Especially if you expect it to run on a bunch of platforms with compilers of varying qualities. Last time I saw code last this, we certainly couldn't rely on compilers being particularly smart, and we did care about speed.
Do you have a pointer to the relevant part of OCaml's source? The whole thing is probably worth a read, but don't think I can set aside enough time for that soon, and it could take me a while to find the right part.
See the "Next" macro definition.
To be fair, I've seen code like this exactly once.
static void* address = &address;
This is helpful for things that take void* context pointers that you want to match with logical equality.
In my practice these exceptions are ubiquitous. Multiple tiny interpreted DSLs (which cannot be compiled more efficiently for the latency reasons), efficient protocols, all that stuff. System programming, in other words, and it's exactly the stuff Rust was supposed to be designed for.
> But it's definitely not the rule and the speed difference between the one and the other is so small
2-4 times difference is not "small" in system-level programming.
> Even the inner loop of an interpreter usually calls routines
These are too high level interpreters for the too dynamic languages, they're beyond any hope in terms of performance, by design. I'm talking about some simpler and better designed things (like OCaml, for example).
In your example:
void (*[10])() function_table = {
&perform_addition,
&perform_multiplication,
...
}
while(1) {
ins = fetch_next_inst();
*(function_table[ins])();
}
Edit: Seeing it written like this now, it's clear you could save yet another jump by defining a macro for what's in the while loop and putting it at the end of every function call.If you were to inline all of the perform functions in the GOTO version vs putting a macro at the end of each function, you're right that there's some function overhead, but I think it's as small as a single instruction to put the return address on the stack. Maybe that would be optimized away, maybe not.
To your point: My argument isn't that you can't do the GOTO version. With optimizations, it's essentially identical. My point is, that is a lot more hard-to-grok code to maintain for something that can be achieved in a simpler way.
Sometimes this can be optimized away, but not always.
Over time it tends to evolve into ever messier and harder to understand versions of the initial run after which a future maintainer will end up losing sleep and or hair chasing some production bug.
Consider this (slower!) much clearer alternative, and consider too that if you need to eliminate one goto for speed reasons that you're most likely doing something wrong:
typedef enum { ADD, MUL, ..., END } opcode;
int process_instruction(opcode ins) {
switch (ins) {
case ADD:
perform_addition();
break;
case MUL:
perform_multiplication();
break;
...
case END:
return FALSE;
}
return TRUE;
}
void run() {
do {
} while (process_instruction(fetch_next_instruction());
wrap_up();
}
That's a whole function call overhead (but you can eliminate that with an 'inline'), it's easier to test and much easier to follow what it does.It would be interesting to see what the actual difference is in speed when comparing those two versions, I suspect that the contents of 'perform_addition' and 'perform_multiplication' are going to be the key here, not whether or not the loop uses a short-cut or an instruction (or two) less. Oh, and you could have eliminated that 'ins' variable.
In summary, the assembly of the goto version is much shorter and there's a 10% speedup over SPEC JVM98. The optimization works out because: a) instruction bodies are often short; b) instruction dispatch is the hottest code path of any interpreter; c) it's orthogonal to all or almost all other optimizations.
Exactly. And stop-gap measures are fine if and when they're followed up by a proper solution. Unfortunately stop-gap measures tend to be a lot more permanent than originally intended in practice.
I like the way this is handled in Go with the defer keyword: https://blog.golang.org/defer-panic-and-recover
This construct gives you most of the power of C++ RAII without the overhead. Except that you can't use it to cleanup resources after exiting an anonymous block—it strictly defers to function exit.
In all other cases (which is almost all usages), it is semantically equivalent to RAII, so the language doesn't force any runtime overhead. The only difference is that the compiler is less mature than an average C++ compiler, but this is an implementation problem, not a design problem.
RAII has no overhead because it is a pattern designed within the context of a zero-overhead language. defer allows you to implement a superset of cases that RAII handles, including those with runtime overhead.
I hadn't considered the type of overhead you're talking about, which is admittedly more important for the kind of software that tends to get written in C.
But it leaves out the most useful part: in C++ releasing the resources is completely implicit:
{
std::ifstream in(fn);
// Do something with 'in'
} // Released
while with Go defer, you have to call explicitly for the method that you want to call. If you forget this, you still have a resource leak.Sure, it's an improvement over languages where you have to cover every possible scope exit, but it definitely doesn't give you 'most of the power' of C++ RAII.