Ken Thompson does go into more detail in a link off the SO question I posted earlier.
[edit: i see you've already spotted that link. Sorry about that]
I get that serious emulators invest effort in making dispatch fast, and that a naive for/switch loop is not the fastest way to dispatch instructions, but it's nice for getting the emulator working. :)
If you're the kind of masochist that enjoys optimizing dispatch loops (hi, you're not alone), check this out: http://www.emulators.com/docs/nx25_nostradamus.htm One of the best resources out there for this sort of stuff.
That article explains how to implement the technique for C programs using a "computed goto"/"labels as values" gcc extension. While Go lacks that feature, dispatching a table of functions as the last statement in each opcode implementation should yield a similar result. As long as Go supports "tail-call optimization" in the trivial case of "functions that take no parameters and return nothing calling similar functions" it should work just fine. Googling suggests that Go does not support TCO, but at least this program didn't explode the stack:
EDIT: Yes, it does.
package main
import ("fmt")
var acc byte
func main() {
fmt.Println(acc)
acc++
main()
}It does, it's just that Go is so slow printing to the console that it would take years to run out of stack space. If you redirect to null it will use up all your memory and swap space in a few minutes:
./main > /dev/null
In most other languages this same code would run for a short time and then abort after exhausting the stack. This is the best behavior since algorithms that use unbounded memory are where you certainly must handle out of memory errors and set limits; using too much stack space is an error that should be caught quickly not postponed. Go on the other hand uses a growable stack, so the code you gave will use up all available memory and swap before finally crashing.
Go uses a growable stack so that programs can use many goroutines on 32-bit machines. This is bad for performance due to extra checks on calling function to see if the stack needs to be grown or shrunk, the overhead to actually do that, and less efficient use of cpu data cache. It makes it complicated to call functions from any other language. It seems like any modern language should work best on 64-bit and make trade-offs for 32-bit, not the other way.
Growable stacks aren't about getting optimal performance on 32 bit architectures. That is explicitly a non-goal of Go. They're about minimizing memory consumption for goroutines which don't use very much stack space, which is expected to be most goroutines. You can't have hundreds of thousands of goroutines if you have a high fixed amount of memory per goroutine.
As for your argument that growable stacks make it harder to determine program correctness, it seems like nonsense to me. I could make the same arguments about heap space, but nobody thinks a low fixed limit on heap sizes is a great idea. If you want to test your program under low memory conditions, try mlocking a lot of memory and then running your program. Alternately you could try something involving cgroups or virtual machines.
Can anyone think of any other way to implement this kind of dispatch system in Go? If it isn't possible, I don't think Go deserves the status of being "a good language to write emulators in," as this is a pretty important technique for improving performance of CPU emulation.
By the way, I did enjoy reading your link about computed gotos. It sounds like what it comes down to is that the C switch statement does bounds checking, and the computed goto can avoid that. At the end of the day, though, no matter how many hacks you pile on, using an interpreter will have an overhead above dynorec.
N.B. Tail call optimization has its costs as well as benefits. Rust recently announced that it won't be supporting it. I don't know if the Go guys have made any statement about this, but I would imagine they might not consider it, for the same reasons. More here: https://mail.mozilla.org/pipermail/rust-dev/2013-April/00355...
I'd probably rank that compile time 'error' as one of the most annoying I've seen to date because the code was actually fine, it was just the compiler pre-empting a non-existent risk. So I had to rewrite a chunk of code just for the sake of over-eager error catching (this is also why I wish go build would just warn about one or two trivial issues instead of flat out fail)
EDIT: Missed the link: https://groups.google.com/forum/#!msg/golang-nuts/IURR4Z2SY7...
Seems like a reasonable argument, but then again I don't see why they even bothered adding a switch given those constraints.