Comparing Kotlin and Go implementations of the Monkey language
First of all, what is the Monkey language?
Monkey is a language created by Thorsten Ball as part of his books Writing An Interpreter in Go and Writing A Compiler in Go.
Both books are excellent, and I cannot recommend them enough to my fellow programmers/developers interested in programming language design.
I wrote an implementation in Go, following both books chapter by chapter. I also wrote an implementation in Kotlin, translating my own code from Go to Kotlin.
Disclaimer
This was my first serious adventure with Go. I have no other experience with the language; on the other hand, I consider myself a Kotlin expert (I even co-authored a book a few years ago). This is to say that I’m biased towards Kotlin, and I know a lot more tricks on it, but I’ll try to be as fair as possible with my comparison.
Some of the things that I dislike about Go can be fixed by specific patterns or libraries or the introduction of generics (at the time of writing this article, the latest Go GA version is 1.17.x,) but as a complete newbie to the language, I don’t have access to that knowledge yet.
Where Kotlin shines
Expressiveness
Kotlin is a language more expressive than Go, meaning that with less code, you can express the same ideas (the caveat is that it can be hard to read for the uninitiated). For example, my Go implementation is 6278 lines of effective code (no blank lines nor comments), compared to Kotlin with 4663 lines of effective code (both implementations have a healthy dose of tests).
I know, I know, it doesn’t matter… until it does, fewer lines of code, fewer possible bugs, less code to maintain.
But explicitly, where is the difference? I’m glad that you asked.
Types (and Generics)
It seems like checking Go types in runtime is complicated. The language comes with a construct name type switch, and/or you can try to cast to a type and then capture an error if the cast failed (more on errors later).
So this Go code:
Is equivalent to this Kotlin code:
You can say that is just a few lines more, but now I can reuse the checkType function everywhere (42 times on my codebase!) and save more lines, and I can argue that it is easy to read as well.
Now comparing the type of one value to another value is a different story, and the pattern used in this book is a form enumeration comparison:
Now we declare a type implementing the Object interface.
And we can compare the types using the ObjectType enum.
None of this is necessary for Kotlin:
We don’t need an enum to compare types
And therefore, our type is a lot simpler.
And we can use the types directly, including smart casting.
This leads to my last point:
Errors:
I know I’m kicking the proverbial dead horse, but by using exceptions, I’m saving a lot of lines of code:
As I said before, I’m not an expert on Go, and maybe there are different patterns and/or libraries to reduce the code pollution caused by Go’s error management.
Where Go shines
Byte operations
Byte operations in Go are a breeze. First of all, there is a proper unsigned byte type.
Kotlin does have an UByte type but is considered experimental (at the time of writing this article, the Kotlin GA version is 1.5.30), meaning that you need to opt-in your codebase and as an option in the compiler.
All the UByte operations are clunky, and I ended up implementing many things such as slices and others. Also, the bitwise operations in Kotlin have a different name, e.g. >>> is ushr, yes, really.
This leads me to my final point:
Performance (with a caveat)
A full implementation of Monkey comes with two execution modes, eval (a tree walker interpreter) and vm (bytecode compiler + virtual machine).
And the way how we test it is using a fibonacci(35) execution, that in Monkey looks like this:
Nice, let’s running on eval mode for Golang
./fibonacci -engine=eval
engine=eval, result=9227465, duration=26.739988454s
26.73 seconds, not bad
Let’s try Kotlin now
$ ./monkey.sh eval
engine=eval, result=9227465, duration=11.931564613s
Wow, mate. Kotlin blows Golang out of the water, almost three times faster.
My theory is that the JVM is using JIT and other techniques to increase the performance of certain parts of the code.
Now let’s try running on vm mode for Golang
./fibonacci -engine=vm
engine=vm, result=9227465, duration=5.891256054s
Ufff, almost 6 times faster than the eval mode and twice times faster than the Kotlin eval mode.
I cannot wait to see how fast is Kotlin on VM mode!!!!
And literally, I didn’t wait… defeated; I stopped the execution after 7 hours… yes, you read me right, 7 hours.
I used VisualVM to try to check what my code was doing.
It turns out that add new elements to a List<UByte> (for the stack representation of my VM) is a very slow operation. Using List<T> in Kotlin is idiomatic but not very performant for my specific case.
Then I replaced my stack from List<UByte> to UByteArray a specialised version of Array<UByte>. (I also replace another List<T>s to Arrays).
Rerunning it…. 53.xx seconds??? Wow, quite the improvement, but still far from perfect.
I reran my VisualVM. It turns out that the functions that I wrote to implement slices were using List<UByte> behind the scenes (a common pattern in the Standard Kotlin library). I reimplemented those functions, avoiding List<UByte> as much as possible and using UByteArray instead.
$ ./monkey.sh vm
engine=vm, result=9227465, duration=11.367100494s
From 7+ hours (it can be a lot more than that) to 11.36 seconds. very fast…. But just 10% faster than my eval implementation and half as fast as the Go implementation.
Conclusion
As always, there is no golden hammer, and each language has its use cases. I still prefer Kotlin over Go, but if I need to punch some bytes, Go is the way to go.
There are still two options to make Kotlin faster than Go, GraalVM and Kotlin native, but that is for the next post.
Update 1: September 19 2021
The previous version of this article use Golang instead of Go to refer to the language. I know that the language is Go, I just wanted those sweet, sweet clicks from SEO. (Seriously Google, you created a language and you gave it the worst possible name to search for it)