Spend some time using Go, and you will be impressed by its performance.
You'll wonder, "Were all those haters on Hacker News misinformed?"
Spend some time using Go, and you will be impressed by its performance.
You'll wonder, "Were all those haters on Hacker News misinformed?"
And just like in C, if you want to avoid memory management overhead you can use a slice of structs, integers instead of pointers, and a freelist (if needed). For example, here is a pointerless sparse bit vector:
https://bugfix-66.com/7256e0772dc3b02d72abf15b171731c933fd44...
The article is storing parses in a balanced binary tree, like a packrat memoizing parser.
Here is the fastest balanced search tree in Go. It allocates (and uses Go's garbage collector) but you can easily use a slice of structs with integer index pointers and a freelist instead:
https://bugfix-66.com/c93e950965804eba90a34e0055985b1c42d5a1...
The above code will perform very similarly to C.
> Keeping everything in the same language has benefits like greatly simplified tooling and building
Surely there are other Go libraries that incorporate C, C++, or Rust? Also if both parsers existed and were equally easy to set up, and you were planning on doing a ton of parsing, it would make sense to go with the faster one.
If you'd like to, you can provide some real-world examples - or even microbenchmarks - showing that Go is so much slower than <your choice here> that it's going to make a difference.
> Also if both parsers existed and were equally easy to set up
They're not equally easy to set up. Language interop is a pain in the pass.
https://github.com/aretext/aretext/blob/main/Makefile
Build is literally a `go build ...` and install is `go install`. Adding any other language to the mix would make this a polyglot project and not be "equally easy to set up". The other question is, do both parsers exist? In this write-up they point to tree-sitter as a possibility which is a JS program that produces C code. This would be viable, but here's the author's take:
> I considered integrating tree-sitter, an incremental parsing library with parsers for many existing languages. However, running JavaScript to generate parsers and linking to a C library would have greatly complicated the build process. Today, aretext can be built on almost any platform using a single go install command. I’ve had users install aretext on ARM laptops, FreeBSD servers, Chromebooks, and Android phones. To maintain portability, I wanted a pure Go implementation.
So this wasn't some casual decision, but something they at least considered long enough to describe here.
And the parsing library itself is only around 1200 lines total (comments, blanks, and code). The parsers for each language add a lot more, of course, but should be roughly equivalent given the same library and interface. I imagine that if this project really takes off and performance becomes a real problem they can do the rewrite at that point. Right now, the code works, seems to work fast enough for its author and primary users, and it's trivial to install on any platform supported by Go. So yes, it would have been a premature optimization to complicate the build process, probably reduce the number of supported platforms (or greatly increase the effort to support the same number of platforms), just to have a slightly faster parser.
Any language is fast enough to do this, certainly Go is. Naive parser combinators written in slow languages can tokenize six-figure LOC files fast enough that the user won't notice.
Rust with a bit of effort put into optimization will be faster than Go with a bit of effort put into optimization, it is true. However, you need to double-check your intuition for how big and how consequential the delta is, because I'd guesstimate it as roughly a factor of two, possibly a touch less. It is true that Rust does a crapton more "optimizations", but a lot of those optimizations have diminishing returns.
A factor of 2 may still sound large, but in practice it isn't as large as it sounds, because my qualification "a bit of effort put into optimization" is not redundant. Go with a bit of optimization will probably beat someone's first draft of Rust. Go with a ton of careful optimization will probably beat Rust with a bit of optimization. The raw performance of the two are reasonably close, and smaller than the improvements you can usually get with optimization. So Rust's speed advantage, which is real, generally only matters in cases where you're going to optimize very heavily.
Is this one of them? For that I can give a solid... Maybe! There are times and places where parsing is something you want optimized to within an inch of its life, certainly. However... it isn't all the places, and some of your intuitions may lead you astray if you're not careful; you might think a heavy duty programming language would need a great parser, but if it's going to chew on optimizations for possibly literally 100x the time, it may matter a lot less.
In general, Rust is capable of going faster than Go (again, I'd guestimate about a factor of 2 with isolate tasks where it may be able to go event faster, but that only matters if that's the bulk of your program), but Go is going to be fast enough that that only matters in certain limited places where you're willing to put some non-trivial effort into performance in the first place.
This is in contrast to a comparison between Go/Rust and Python, where even casually written Go/Rust can outpace optimized pure Python, even before we start talking about how much better Go/Rust will be using multiple CPUs. This is because Python is just that slow, and let's not even talk about how slow Python can be if you don't write it to be optimized and you start heavily using all its features without realizing how expensive they are. From the point of view of Python, Go and Rust have very similar performance characteristics. (But then, of course, one must be careful with Python because something like NumPy will blow your socks off when it turns out to not really be Python at all.)
It's a rich, nuanced problem space and should not be approached with sloganeering and "my language is better than yours".
My summary of Go is: It's a slow compiled language... but it is still a compiled language, and it is faster than pretty much everything that isn't, possible exception of LuaJIT, and the delta between slowest compiled and fastest compiled is only ~2-3x, which in the overall space of programming language speed isn't actually that great.
Java tends to win for really naive programs where the author didn’t bother caring about performance or allocations at all, but if any care was put into it at all Go usually wins in my experience.
The trope that Go’s GC is primitive in comparison to Javas is not really accurate. You can’t consider a language’s GC in isolation.
Java’s GC and JIT are extremely complex because the language semantics are terrible for performance by default. The “everything is an object” model made sense when the language was designed and main memory access times were roughly equal to a CPU cycles, but that’s no longer true by a factor 100 to 200 now.
Go’s GC makes different trade offs (low latency, extremely high concurrency, tight integration with the runtime and scheduler) because the language semantics are much more sympathetic to modern hardware (“true” structs, automatic escape analysis, etc), so it can.
See how ahead Java is of any other managed language (and it doesn’t really make sense to do this benchmark with non-GCd languages)? Though this is done with the G1 GC, not the low-latency one - this default GC is more throughput oriented with a max pause time target goal. Also note how Java does use multiple times more memory, as it “postpones running GC when it knows it still will have enough time to collect all of it without running out of the target goal” - this is also the reason why java is quite ahead on “energy efficiency” reports as well. And also, GCs work better with more heap space.
Very soon businesses would be asking for "dollar efficiency" also. I think going by effort on Java and their frameworks vendors to pack more instances of Java process/pods on a VM, it is already been asked by tech savvy customers.
So that old fact that on sever side programing customers only care for raw throughput and not on machine size because RAM/CPU/disk is cheap is not working well in cloud based deployments where now each of these matter.
I pay like 5 dollars a month for a VM with very low params, but even that will happily run anything. Especially that the DB likely can’t be shared the bus factor will remain 1.
I agree on that. And the bureaucracy evolved around "Microservice Architecture" of Kube pods, service mesh and so many other pieces required for it feel like something we are trying to do it well, what should not be done in first place.
For example, the fact that the Java GC is copying and generational means that there is a LOT more overhead introduced by write barriers.
If you benchmark the rate at which the GCs can clean up garbage, Java always wins, but the Java GC impairs you a lot more in other situations that the Go one doesn’t.
It’s trade offs, but the Go one makes much better trade offs for modern hardware IMO.
ZGC (the low-lat GC) does employ read barriers though which will decrease throughput considerably, but latency and throughput are almost universally opposites of each other.
This is the kind of thing always offered without any serious numbers extracted from real life or even realistic test programs.
So even if technically true in very narrow sense it is more of high performance car marketing with fancy algorithm and data structure names. By the time GC are used in end user programs with tons of libraries, frameworks, design patterns and inefficient to implement business rules those GCs show little difference that fancy ads promised on TV.
Also, Java’s GCs are moving/compacting GCs, so while the immediate memory representation is indeed inefficient, again, for long running processes Java will place often together-used objects physically close to each other, and will defragment the heap. But Valhalla can’t come soon enough, I agree.
> Relying so heavily on the JIT also has other problems especially in programs that have widely varying execution paths
Has it? I would think that an AOT program would have a worse time with widely varying execution paths, while a JIT compiler is free to reoptimise based on changing application state.
I mean it feels like personal choice. Do I praise the spouse when they bring whole kitchen down while making a dish and cleaning up quickly afterwards? Or do I take it as "Well, you made mess so it was basic expectation from you to clean up fast for later use"
A real world example of this is esbuild. The author posted on HN that his initial Rust implementation was actually somewhat slower than the subsequent Go version.
I'm not supporting the argument that everything should be written in Rust (or whatever) for good performance. However blanket statement like this is not true; micro-benchmarks are often misleading. There are many factors which affect the performance and they come with tradeoffs, so you can choose what options favor you most. At the end, objectively Rust offers more ways to optimize your program.
> esbuild That's an interesting situation. I'll have to take the author's word here since rust's version is not available for us to see. I write both go and rust (go for living and rust for side projects), I can see some situation where a naive implementation in go could perform better than naive implementation in rust (assuming both are implementing same algorithm). Again rust provides options to mitigate performance problems once you decide to optimize a bottleneck, but if you touch the upper ceiling in go program, there is not much you can do about it.
> , but if you touch the upper ceiling in go program, there is not much you can do about it
I’m not seeing this. There’s lots you can do to optimize Go code. Could you give a concrete example?