On Mon, Jun 8, 2020 at 10:14 AM Andy Kosela <akosela@andykosela.com> wrote:
On 6/8/20, Dan Cross <crossd@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

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:

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) }

it works on everything from retro 8-bit micros to modern big HPC

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.