However, if you really want high performance, you can go ahead and write it in C with a custom matching code and a custom data structure for keeping your word count. This will most likely result in hundreds of lines of code, and enough performance to max out a PCIe SSD. However that would be an different exercise.
Second, it's not clear to me at all why this problem asks for regexps. Both examples used a regexp to check a file extension, and then to find word boundaries. Both are trivial† to code directly.
Am I misunderstanding the problem here? It seems like there's hardly any string manipulation in it at all.
† admittedly, I didn't bother being careful about word boundaries
I know some C++ folks were annoyed it’s using regex due to criticisms of the standard regex libraries that can’t be fixed, or something, so it’s also not like folks agree that the original solution was optimal. I don’t think it was trying to be, so seems fine, but it is what it is.
I'm fed up with C dependencies, which like Makefiles, always seem to be very easy in principle, and then kill by thousand cuts (like a regex library that defines a symbol that happens to conflict with a POSIX regex function, which I didn't even use, but it corrupted memory of a completely different dependency elsewhere).
This particular program needs no third party dependencies at all; in fact, I bet it'd get longer if I added them (like a glib hash table, or pcre).
Using a different technique, for example by not using regexp is a bit like cheating in that context. You are not comparing two languages, you are comparing two different solutions to the problem.
That's what I meant with my previous post. Either you do it as specified in the article, with regex and maps, which require pulling libraries and working in a way that is much less convenient than with the C++ and Rust example for no good reason. Or you reimplement it using different techniques and this is not the point of the article.
Or to put it simply, the example in the article is not good for C.
So, no, I don't think you're right about this.
More to the point, though: I'm talking about regex libraries because the parent comment is. My point: the Rust example uses 3p dependencies, so what C does "out of the box" is already out the window.
https://gist.github.com/tqbf/4de61a3e34d2e4664044666c107abe7...
(I don't vouch for this code; I wrote it off the top of my head, and, in keeping with the exercise, I wrote it in pico).
It's not 10x longer. I'm honestly not sure why either Rust or C++ bothered with regexps for this problem.
Anyways, you get the gist of what this looks like in C now. Obviously, don't write things like this in C.
With that fixed, this dumb program does my whole (very large) homedir in about 10 seconds, for some definition of "does" that may or may not include counting every word in every txt file. For the curious:
0. get / 566006
1. the / 158168
2. pkg / 119419
3. syscall / 105828
4. const / 98259
5. and / 86670
6. ideal-int / 43078
7. that / 31609
8. for / 31129
9. this / 24980
10. text / 22907
:P while (fgets(buf, 1024, fp)) {
char *c, word[1024], *w = word;
for (c = buf; *c; c++) {
*c = tolower(*c);
if (*c >= 'a' && *c <= 'z')
*w++ = *c;
else {
*w = '\0';
count(word, (size_t)(w - word));
w = word;
}
}
*w = '\0';
count(word, (size_t)(w - word));
}
and you have that `count` function skip words with len < 2 then I can match results with the other 3 (Nim, Rust, C++) versions. Also, your C version runs in just 7 ms on Tale Of Two Cities, about half the time of the PGO optimized Nim.I thought about also keeping the top 10 as I go instead of copying the whole table. But I'm guessing that virtually all the time this program spends is in I/O.
I just did a profile and saw about 15% in strcmp in the hot cache run, but sure if it's not in RAM then IO is likely the bottleneck.
This C code is not only not protecting you against that, but also it seems to have little to none error checking. What happens if fopen fails? It just skips to the next file and happily ignores all the entries in the file. What if fget fails? Again, it stops processing the file and goes to the next.
I won't even get into what happens if calloc fails and returns null. Or if ++ wraps around and you get a negative value (let's hope we are using 64 bits)
This program will likely work, but if it doesn't you will just get an invalid number and never find out.
Don't get me wrong, I understand this is "not serious" code, written in a Sunday and for fun. And I would be ok with it if the article wasn't about language safety.
(I think you estimated badly, for what it's worth.)
:P
You can golf out about 10 lines of this by getting rid of the table free. :)
I could have done a number of things to keep the line count down–in the spirit of the challenge I tried to keep it clean, pedantic, idiomatic POSIX C because if I didn't I'm sure someone would have jumped on it for it not being that :P So it's written in nano in the style of how I might write a homework assignment rather than something I specifically golfed. In retrospect the fixed-depth buckets probably added more complexity than they saved, and using a linked list or open addressing would have probably been much easier since it'd clean up some of the resizing and traversal code. Plus, the load factor was in general fairly poor, the final table (when I ran it on my Downlods folder) had just over 8 million buckets of depth 5 allocated and only about 600k got used at all, and of that 570k were filled just once.
Oh, and here's the results on my Downloads folder:
514464 Developer
425236 Xcode
386616 saagarjha
386075 Users
361324 Library
337374 build
333056 Wno
306758 WebKit
302892 DerivedData
296179 eugbibmfmfphgbhczsxiimkhynol
I have a couple WebKit build logs that dominated the results…