On Mon, Jun 8, 2020 at 10:14 AM Andy Kosela <akosela(a)andykosela.com> wrote:
On 6/8/20, Dan Cross <crossd(a)gmail.com> wrote:
[snip]
The real question is can Go be
really faster than C/C++? The last
time I checked it was at least two times slower on an average.
This is an unanswerable question, because it lacks enough detail to really
answer the question honestly: what does it mean to be "faster"? It really
depends on the problem being solved.
If I'm writing straight-line matrix multiplication code on
statically-allocated structures, I see little reason why Go would be
inherently slower than C, other than that the compiler perhaps isn't as
advanced. If my program is a systems-y sort of thing that's frequently
blocked on IO anyway, then e.g. GC overhead may not matter at all. If my
program is highly concurrent, then perhaps I can bring some of the
concurrency primitives baked into Go to bear and the result may be closer
to correct than a comparable C program which lacks things like channels as
synchronization primitives and light-weight threads a la goroutines.
Putting that last point another way, more expressive abstractions allow me
to develop a correct solution faster.
As an example of this, for my current project at work, I build a custom
board that lets me control the power and reset circuitry on ATX
motherboards; think of it as a remote-controlled solid-state switch in
parallel with the motherboard power and reset switches. Each board can
control up to 8 other computers. The control interface to that board is a
USB<->UART bridge; USB also provides power. Since I'm controlling multiple
other machines, possibly in parallel, I wrote an interface daemon that
talks to the firmware on the board itself but presents a high-level
abstraction ("reboot the host named 'foo'"). I wrote it in Go; a
single
go-routine owns the serial device and accepts commands corresponding to
each system on a channel; controlling each system itself is a goroutine.
The whole things speaks a simple JSON-based RPC protocol over a Unix domain
socket. It's possible to control any of those 8 machines in parallel. It
was only 200 lines of code and worked correctly almost immediately; I was
really impressed with how _easy_ it was to write. I dare say a similar
program in C would have been much more difficult to write.
Further, while GC undoubtedly has overhead, it gives rise to optimization
alternatives that aren't available in non-GC languages. For example,
instead of atomically incrementing or decrementing a reference count in a
tight loop before manipulating some shared data structure, I don't bother
in a GC'd language; the offending memory will be automatically reclaimed
when it's no longer referenced elsewhere in the program.
Here's a really well-written post from 2011 about profiling and optimizing
a Go program, and comparing the result to (approximately) the same program
written in several other languages:
https://blog.golang.org/pprof
Plus it will not run on older systems; I think you need at least kernel
2.6.23 and its executables are rather big compared to
C.
Now we have to differentiate between what we do for fun and what we do
professionally. I don't care about targeting new software I write
professionally at old versions of Linux or obsolete hardware platforms.
Storage devices are huge, and the size of executables isn't really a
concern for the sort of hosted software I write in Go. It may be for some
domains, but no one said that Go is for _everything_.
In the
beginning it was fun to create some clones of classic
Unix tools in
Go, but in reality they were still slower and bigger than the original
utilities.
I'm not sure I buy this. Here's an example.
I've never liked the `cut` command; I've always thought it contained some
really odd corner cases and didn't generally solve some of the problems I
had. I've usually been satisfied with using `awk` to do things like pull
fields out of lines of text, but in the spirit of laziness that typifies
the average Unix programmer, I wanted to type less, so I had a little Perl
script that I called "field" that I carted around with me. One could type
"field 1" and get the first field out of a line; etc. The default delimiter
was whitespace, it could handle leading and trailing blanks, separators
could be specified by regular expressions, etc.
When the Harvey project got off the ground, one of the first things done
was retiring the aged "APE", which meant that awk was gone too and of
course there was no Perl, so I rewrote `field` in C:
https://github.com/Harvey-OS/harvey/blob/master/sys/src/cmd/field.c
It works, but has issues. But I got to rewrite it in Go for the u-root
project:
https://github.com/u-root/u-root/blob/master/cmds/exp/field/field.go
Not only is the Go rewrite a little shorter (-75 lines, or around 12%
shorter than the C version), I would argue it's simpler and probably a
little faster: instead of rolling my own slices in C, I just use what the
language gives me.
I jumped on the Go bandwagon in 2010 (like a lot of people from the
open source community) but recently I find myself
coming back more and
more to C. It is still a superior language in almost all areas.
C is my first love as a programming language, but I really don't think one
can say that it's "still a superior language in almost all areas."
It's
just too hard to get some things right in it, and programmers who think
that they can do so are often deluding themselves.
Here's an example:
unsigned short mul(unsigned short a, unsigned short b) { return a * b; }
Is this function well-defined? Actually, no; not for all inputs at least.
Why? Consider a 32-bit target environment with 16-bit `short`. Since the
type `short` is of lesser rank than `int`, when the multiplication is
executed, both 'a' and 'b' will be converted by the "usual
arithmetic
conversions" to (signed) `int`; if both 'a' and 'b' are, say,
60,0000, then
the product will overflow a signed `int`, which is undefined behavior. The
solution to this is simple enough, just rewrite it as:
unsigned short
mul(unsigned short a, unsigned short b)
{
unsigned int aa = a;
unsigned int bb = b;
return aa * bb;
}
But it's shenanigans like this that make using C really dangerous.
Thankfully, Go doesn't have these kinds of issues, and the Rust people by
and large avoid them as well by mandating explicit type conversions and
explicit requests for modular arithmetic for unsigned types. In Rust, this
would be:
fn mul(a: u16, b: u16) -> u16 { a.wrapping_mul(b) }
Plus
it works on everything from retro 8-bit micros to
modern big HPC
systems.
That's a fair point, but I would counter that different tools are useful
for different things. If I want to target an 8-bit micro, C may be the
right tool for the job, but Go almost certainly wouldn't. I'd likely choose
Rust instead, if I could, though.
- Dan C.