Alright. I write programs as a hobby. I work as a sysadmin, so I'm not a
"full blown developer". It's mostly a hobby. Sure, I write lots of code
at work, but it's a different experience than a dev has. So, I might not
have a "standard" perspective on things here.
These are the tools that I currently use:
- Shell: For simple stuff. Combining a bunch of pre-existing programs.
- Python: For more complex stuff where performance doesn't matter.
- C: When performance or minimalism matters.
(I was using Shell for *much more* in the past, but that's a different
story.)
I'm happy-ish with the first two, but C is a problem. It is arrogant to
think that I can "master" C. There is so much undefined behaviour.
Maybe other people can keep track of that in their head, but I can't. My
C programs are usually as simple as possible and I read them a million
times to make "sure" I didn't mess up. External tools like valgrind can
help, but they don't solve the problem entirely.
So, for a while now, I've been looking for alternatives.
Rust and Go are the obvious choices. At least that's what people tell
you. "Go is a modern C!" "Rust is a modern C++!"
Rust is incredibly hard to learn (which, again, is a story for another
day), but at least it is fast. It's a bit slower than C in my
experience, which is to be expected, but it's more or less similar-ish.
Rust has other issues like a very small standard library (*yet* another
story!) and, ugh, it's so hard.
So, how about Go then? If it's a "modern C", isn't that what I want?
The thing is: Those classifications only apply to *the language*.
Specifically, Go has garbage collection at runtime -- and that is pretty
costly. So costly, in fact, that I wonder if I can make use of Go at
all.
Here's an example:
-
https://uninformativ.de/git/buffyboxes
-
https://uninformativ.de/git/go-buffyboxes
It's a program that scans my ~/Mail for maildir directories with unseen
mails in them. The first one is implemented in Rust, the second one in
Go.
The Go version was super easy to write. Go is so much easier to use than
Rust, it's a delight. This is, indeed, like comparing C and C++.
But let's have a look at performance (this is "perf stat ..." on Linux).
First, here's Rust:
Performance counter stats for 'rust-buffyboxes':
134.81 msec task-clock:u # 0.994 CPUs utilized
0 context-switches:u # 0.000 /sec
0 cpu-migrations:u # 0.000 /sec
106 page-faults:u # 786.297 /sec
181,268,933 cycles:u # 1.345 GHz
349,395,875 stalled-cycles-frontend:u # 192.75% frontend cycles idle
413,494,813 instructions:u # 2.28 insn per cycle
# 0.84 stalled cycles per insn
84,339,686 branches:u # 625.623 M/sec
264,641 branch-misses:u # 0.31% of all branches
0.135689355 seconds time elapsed
0.039639000 seconds user
0.095743000 seconds sys
Now Go:
Performance counter stats for 'go-buffyboxes':
325.31 msec task-clock:u # 1.081 CPUs utilized
0 context-switches:u # 0.000 /sec
0 cpu-migrations:u # 0.000 /sec
3,226 page-faults:u # 9.917 K/sec
576,142,657 cycles:u # 1.771 GHz
537,322,281 stalled-cycles-frontend:u # 93.26% frontend cycles idle
1,059,866,178 instructions:u # 1.84 insn per cycle
# 0.51 stalled cycles per insn
246,705,344 branches:u # 758.371 M/sec
2,083,503 branch-misses:u # 0.84% of all branches
0.300841964 seconds time elapsed
0.242355000 seconds user
0.094261000 seconds sys
Wallclock runtime is 0.135s in Rust vs. 0.3s in Go. The number of
instructions issued and hence the required number of CPU cycles is more
than twice as high in Go. More branches. "Seconds spent in user space"
goes up by a factor of 6.
Plot twist, here's my original implementation of the same program in
Python:
Performance counter stats for 'python-buffyboxes':
230.30 msec task-clock:u # 0.998 CPUs utilized
0 context-switches:u # 0.000 /sec
0 cpu-migrations:u # 0.000 /sec
3,056 page-faults:u # 13.270 K/sec
530,611,901 cycles:u # 2.304 GHz
469,803,499 stalled-cycles-frontend:u # 88.54% frontend cycles idle
1,145,782,086 instructions:u # 2.16 insn per cycle
# 0.41 stalled cycles per insn
249,244,466 branches:u # 1.082 G/sec
1,328,806 branch-misses:u # 0.53% of all branches
0.230792465 seconds time elapsed
0.140332000 seconds user
0.090246000 seconds sys
It's in the same range as Go regarding instructions, but overall
actually a little faster.
Long story short: You can't have both things. You can't have a super
simple high-level language with garbage collection at runtime and, at
the same time, blazingly fast performance. I've been really angry with
Rust due to its complexity, but that complexity exists for a reason.
Yes, this is just one example. I have a feeling, though, that it's more
or less representative, because Go's GC won't go away.
Go certainly has its advantages. For me and my use cases, I doubt that I
will be able to replace C with Go, though. I might be able to replace
*Python* with Go. Go is a compiled language and the compiler can catch
lots of mistakes in advance -- this is really annoying about scripting
languages. Then again, Go creates huge binaries and I usually don't need
the static linking, so ... meh. We'll see.
But C? That's probably going to be Rust territory. (For me. Not in
general.)