Beating C with 70 lines of Go
ajeetdsouza.github.io
ajeetdsouza.github.io
$ time wc -w 100m
2266395 100m
real 0m4.568s
$ cc -o wc wc.c
$ time ./wc 100m
2343390 100m
real 0m0.511s
Of course, it disagrees on the answer, because I just used 100M of random data and it doesn't care about wide characters. It gives the same answer as GNU on plain ASCII text.It's not faster because it's better, it's faster because it is doing less.
I can't recall who said it, but this reminds me of this idea: If you don't care about correctness then I can make it as fast as you like.
$ dd if=/dev/urandom of=100m bs=1M count=100 2> /dev/null
$ time wc -w 100m
2319144 100m
real 0m4.543s
user 0m4.512s
sys 0m0.029s
$ time env LC_CTYPE=C wc -w 100m
2308032 100m
real 0m0.842s
user 0m0.842s
sys 0m0.000s
Your program is faster, though.All of these articles are frustrating because they use different environments and test sets and none of the ones I’ve read have posted the test sets up. Some people use random characters, some people use existing files. Some people use files of 1 MiB, some 100 MiB, some several GiB in size. Not only that, but the people programming the replacements don’t even normalize for the difference in machine/processor capability by compiling the competitors and GNU wc from scratch. The system wc is likely to be compiled differently depending on your machine. The multithreaded implementations are going to perform differently depending on if you’re running Chrome when you test the app or not, etc.
This would easily be solved by using the same distribution as a live USB, sharing testing sets, and compiling things from scratch with predefined options, but nobody seems to want to go to that much effort to get coherent comparisons.
I think the earliest(?) entry[1] in this "series" (the one done in Haskell) had their test input shared alongside the source code, and it has been referenced in some others (often multiplied several times over, as the original isn't very big). Beyond that it's ASCII only, the contents matter little.
>nobody seems to want to go to that much effort to get coherent comparisons
I think nobody really wants to get any coherent comparisons, because this thing isn't really a competition between the entries themselves.
[1]: https://github.com/ChrisPenner/wc (see data/big.txt)
I understand your frustrations with the previous posts, but I’ve tried to make my article as unambiguous as possible. Do give it a read, if you have any futher suggestions or comments, I’d be happy to hear them!
Alternatively, you could have a sort of "shootout CI server" where people upload their compiled binaries as Docker images and the CI server runs them against several a random subset from a set of (hidden) fixed test datasets, averaging the results. (Random and hidden such that you can't just overfit against the test set; fixed so that it's still mostly measuring the same thing.)
I think the Netflix Prize sort of worked like this?
The goal is to analyze your needs and pick the language best suited to your task. The goal is not to find one language that excels at everything.
I'm convinced the term "systems language" doesn't have a coherent meaning at this point. See:
https://zenhack.net/2018/07/14/three-funerals-in-the-name-of...
That doesn’t make it a bad language, but it’s not universally applicable either.
Nothing is universally applicable.
But yeah, I certainly wouldn't use it for hard real-time tasks. If you can't tolerate missing deadlines ever, there is a very short list of acceptable tools.
But (and correct me if I'm wrong; it's not my area) HFT doesn't strike me as hard real-time? See also a sibling comment that asks about Jane Street's OCaml use.
It's not like GC pauses are happening constantly; Go programs don't allocate that much, and a well tuned program can go a long time between collections. It likely is appropriate for many soft or firm real time systems. And you can shut the GC off if there are sections where GC really must not happen:
Last I looked (a while ago) it seemed like there wasn't quite enough control on the Go GC to do this effectively. It's very likely that's been fixed by now.
I guess C isn't universally applicable either.
Or in dozen of other GC enabled systems programming languages for that matter.
The difference being evolution, takes care of sorting that out, like in all anti-technology groups throughout mankind history.
Regarding using Go as a real systems language (in the same meaning as C):
- gVisor hypervisor on Google Cloud and Linux sandbox on Chromebooks
- Android GPGPU debugger
- Fuchsia TCP/IP stack and volume management
- Baremetal TinyGo on Arduino Nano33 IoT, Adafruit Circuit Playground Express, BBC micro:bit among many others
- Coreboot firmware
> As a systems language, Go is intended to be used for developer applications like, for example, web servers.
Couldn't find a citation for CLI tools, but that should satisfy you (since people write web servers in Ruby and Node.js too).
That said, it's better to use terms that map better to a set of requirements, such as "hard-realtime" or "soft-realtime".
That said, I think you'll find this SIMD-enhanced wc interesting: https://github.com/expr-fi/fastlwc/
It's really not solving the same problem.
As for having a low-memory footprint, 2 of the 4 implementations I mentioned (one single core, one multi-core) consume less memory than wc, and still run much faster.
Reminds me of the argument that venison tastes better than beef. The argument roughly being, "If you shoot the deer right, drag it home right, gut it right, skin it right, tenderize it right and cook it _just_ right, it'll be _almost_ as good as store-bought frozen beef"
Hope that clarifies things.
Just look at the amount of optimization that has gone into yes: https://www.reddit.com/r/unix/comments/6gxduc/how_is_gnu_yes...
Funny how times change.
On the other hand, I do still love code golf and other speed/size/wonkiness competitions. It's a lot like the early demo scene whose sole effort was to show how much they could do with very limited resources.
type FileReader struct {
File *os.File
LastCharIsSpace bool
sync.Mutex
}
And your Lock() and Unlock() calls on FileReader would just work.What I can see there is that for the same algorithm, the Go version was not so fast. It was comparable. The memory overhead could be caused by the size of the program, as wc has more functionalities.
Then there is compared a totally different algorithm with a false claim "this way a go implementation is faster than a c one". Sure it is, as this is a different implementation of a different algorithm. A fair comparison would be implementing the same algorithm in c and comparing then. I assume the difference wouldn't be huge.
So, generally, I think it's not a fair comparison.
Being a garbage collected language with a runtime, Go certainly cannot match the performance of C, and it was never my point to prove otherwise - obviously, for the same algorithm, the C implementation would be faster. Instead, I was exploring Go to highlight how simple it is to write safe, concurrent code in it.
This can be easily shown, by counting words and lines using wc separately. Word counting decodes characters (to find non-ASCII whitespace that may separate words), whereas line counting just looks for ASCII line separators:
$ time wc -w wiki-large.txt
17794000 wiki-large.txt
wc -w wiki-large.txt 0.48s user 0.02s system 99% cpu 0.496 total
$ time wc -l wiki-large.txt
854100 wiki-large.txt
wc -l wiki-large.txt 0.02s user 0.01s system 99% cpu 0.034 total
So, without character decoding, looking at every byte is ~15 times faster. So, if you'd compile wc without multibyte character support (which would be a fair comparison), it would probably beat Go without any parallelization.As I show in [1], removing this call and replacing it by a character match, gives a speedup of almost 2x.
In the second section, I was able to surpass the performance of the C implementation - by using a read call with a buffer. As I mentioned in the article, the C implementation does the same, and in the interest of a fair benchmark, I set equal buffer sizes for both.
Maybe the Linux wc version you have is better. I don't know. They do exist and Penner's article gave a link to one. I am not sure as you didn't link to the source of your wc. (Edit: I just noticed you did use the OSX wc, you're just running it on Fedora. Sorry about that.)
But in any case using just wall clock time can be deceptive. Here is what I mean. On my machine, a somewhat old Mac Mini with a spinning disk, user CPU is only ~1/5 of the real time. The rest waiting for the OS or the disk.
% for ((i=0;i<250;i++)); do cat /usr/share/dict/words >> a; done
% /usr/bin/time wc -l a
58971500 a
1.24 real 0.28 user 0.64 sysAre you kidding me? This is nothing close to a systems programming language. This isn't much of a comparison at all. wc is a very simple case that doesn't match the complexity of real-world programs. Go comes close here because you're not using the high-level abstractions and that make it useful in the real world. GNU coreutils also tend to focus on having tons of features (compared to BSD/busybox/plan9/other), which can slow them down. If you really want to get competitive, I bet an AVX-512 implementation would be fastest, and that's more doable in C, but this is a bogus comparison in any case. It's just people doing this because they like a specific language.
The second FU is they claim that decisions they made based on personal preferences were technical ones. That's a very insidious lie that programmers make all the time. Insidious because it destroys trust between programmers and managers.
- gVisor hypervisor on Google Cloud and Linux sandbox on Chromebooks
- Android GPGPU debugger
- Fuchsia TCP/IP stack and volume management
- Baremetal TinyGo on Arduino Nano33 IoT, Adafruit Circuit Playground Express, BBC micro:bit among many others
- Coreboot firmware
- Biscuit POSIX like OS
But whatever, the GC-FUD is strong among C devotees.
- gVisor hypervisor on Google Cloud and Linux sandbox on Chromebooks
- Android GPGPU debugger
- Fuchsia TCP/IP stack and volume management
- Baremetal TinyGo on Arduino Nano33 IoT, Adafruit Circuit Playground Express, BBC micro:bit among many others
- Coreboot firmware
- Biscuit POSIX like OS
ISO C does not support AVX-512, and plenty of languages do as language extension.
Stop this nonsense please. This does not show anything even remotely close to systems programming capability of Go. Write a device driver in Go that performs without lagging, benchmark it and then come back.
- gVisor hypervisor on Google Cloud and Linux sandbox on Chromebooks
- Android GPGPU debugger
- Fuchsia TCP/IP stack and volume management
- Baremetal TinyGo on Arduino Nano33 IoT, Adafruit Circuit Playground Express, BBC micro:bit among many others
- Coreboot firmware
- Biscuit POSIX like OS
https://en.wikipedia.org/wiki/System_programming_language#Ma...
It's been done. Performs well.
It's not for performance reasons. I think you misread. Also the driver is pure go now:
I guess that's the size of the executable after it's been loaded into RAM that's so large?
Keep in mind in all comparisons to GNU wc that it does extra work, detecting multi-byte characters and decoding multi-byte characters if present, to correctly count the number of words. perf shows a significant amount of time being spent in multibyte character handling. If you trigger a code path that does not do decoding beyond the byte level, it’s much faster:
$ time wc wiki-large.txt
854100 17794000 105322200 wiki-large.txt
wc wiki-large.txt 0.42s user 0.02s system 99% cpu 0.438 total
$ time wc -l wiki-large.txt
854100 wiki-large.txt
wc -l wiki-large.txt 0.02s user 0.02s system 98% cpu 0.034 total
(wc -l looks at every byte, but does no decoding.)From a quick glance, this is also where the 'Haskell beats C' article fails. It’s comparing apples to oranges, the ByteString implementation does not do the same as GNU/macOS wc and returns incorrect results in the presence of non-ASCII punctuation. The article incorrectly states that wc will handle input as ASCII. Unless you do not use a multi-byte locale, macOS wc uses the combo of mbrtowc and iswspace.
> The default action is equivalent to specifying the -c, -l and -w options.
> -c The number of bytes in each input file is written to the standard output.
> -m The number of characters in each input file is written to the standard output. If the current locale does not support multi-byte characters, this is equivalent to the -c option.
Moreover, I also mentioned in the article that I was using us-ascii encoded text, which means that even -m would have been treated as ASCII text.
Hope that clarifies your issue.
White space characters are the set of characters for which the iswspace(3) function returns true.
That your text is ASCII encoded does not matter, since ASCII is a subset of UTF-8. So at the very least, you need an extra branch to check that a byte's value is smaller than 128 (since any byte that does not start with a leading zero is a multi byte character in UTF-8).
However, if you look at the implementation at
https://opensource.apple.com/source/text_cmds/text_cmds-68/w...
You can see that in this code path it actually uses mbrtowc, so there is also the function call overhead.
FWIW, when this was going around for the first time I took this Darwin version of wc and experimented with setting domulti to const 0, statically removing all paths where it might do wide character stuff. I didn't measure any performance difference to just running it unmodified.
if (iswspace(wch))
by if (wch == L' ' || wch == L'\n' || wch == L'\t' || wch == L'\v' || wch == L'\f')
And I get a ~1.7x speedup: $ time ./wc ../wiki-large.txt
854100 17794000 105322200 ../wiki-large.txt
./wc ../wiki-large.txt 0.47s user 0.02s system 99% cpu 0.490 total
time ./wc2 ../wiki-large.txt
854100 17794000 105322200 ../wiki-large.txt
./wc2 ../wiki-large.txt 0.28s user 0.01s system 99% cpu 0.293 total
Remove unnecessary branching introduced my multi-character handling [1]. This actually resembles the Go code pretty closely. We get a speedup of 1.8x.: $ time ./wc3 ../wiki-large.txt
854100 17794000 105322200 ../wiki-large.txt
./wc3 ../wiki-large.txt 0.25s user 0.01s system 99% cpu 0.267 total
If we take the second table from the article and divide the C result (5.56) by 1.8, the C performance would be ~3.09, which is faster than the Go version (3.72).Edit: for comparison, the Go version from the article:
$ time ./wcgo ../wiki-large.txt
854100 17794000 105322200 ../wiki-large.txt
./wcgo ../wiki-large.txt 0.32s user 0.02s system 100% cpu 0.333 total
So, when removing the multi-byte character white space handling, the C version is indeed faster than the (non-parallelized Go version).[1] https://gist.github.com/danieldk/f8cdaed4ba255fb2954ded50dd2...
if (iswspace(wch))
to something like if (domulti && iswspace(wch))
...
else if (!domulti && isspace(wch))
...
got something like a 10% speedup on my machine. And replacing isspace with an explicit condition like yours is much faster still. I checked, isspace is macro-expanded to a table lookup and a mask, but apparently that's still slower than your explicit check. I'm a bit surprised by this but won't investigate further at the moment.I am sorry for the unclear comments. I'll stop commenting on a phone ;).
Indeed, the code uses iswspace to test all characters, wide or normal. Strange design choice.
I agree, it's really strange. This seems to be inherited by the FreeBSD version, which still does that as well:
https://github.com/freebsd/freebsd/blob/8f9d69492c3da3a8c1ea...
It has the worst of both worlds: it incorrectly counts the number of words when there is non-ASCII whitespace (since mbrtowc is not used), but it pays the penalty of using iswspace. It's also not in correspondence with POSIX, which states:
The wc utility shall consider a word to be a non-zero-length string of characters delimited by white space.
[...]
C_CTYPE
Determine the locale for the interpretation of sequences of bytes of text data as characters (for example, single-byte as opposed to multi-byte characters in arguments and input files) and which characters are defined as white space characters.
What about power consumption?