Live data from Hacker News

Destroying C with 20 lines of Haskell: wc

0xd34df00d.me

61–70 of 74 posts

Re: Destroying C with 20 lines of Haskell: wc

#61

Earlier quoted context omitted.

Reproducing what your code does in the most simple, naive C program possible, I can beat the existing wc utility, taking only around 40% of the time that wc takes. So until you put in locale handling, alternate line endings, option handling, and error handling, I don't see that your post is at all convincing. Quite the opposite. So I look forward to a Haskell version that supports everything the wc has so we can get…

> So until you put in locale handling, alternate line endings, option handling, and error handling, I don't see that your post is at all convincing. That's precisely what the second part would be about. And, if I succeed, IMO, that's where Haskell would really shine (because composability and local reasoning), and where I would be able to claim to achieve something — the stuff in the post we're discussing is indeed t…

Here's the source code for GNU wc - https://git.savannah.gnu.org/gitweb/?p=coreutils.git;a=blob;... .

It's easy to see that it's not the result of 10+ years of low-level optimizations to eek out the most performance.

Your test code probably hits the MB_CUR_MAX>1 path at line 361. (Check your locale setting!)

The main loop is:

   402   if (!in_shift && is_basic (*p))
   403     {
   404       /* Handle most ASCII characters quickly, without calling
   405          mbrtowc().  */
   406       n = 1;
   407       wide_char = *p;
   408       wide = false;
   409     }
    ...
   443   switch (wide_char)
   444     {
   445     case '\n':
   446       lines++;
   447       FALLTHROUGH;
   448     case '\r':
   449     case '\f':
   450       if (linepos > linelength)
   451         linelength = linepos;
   452       linepos = 0;
   453       goto mb_word_separator;
   454     case '\t':
   455       linepos += 8 - (linepos % 8);
   456       goto mb_word_separator;
   457     case ' ':
   458       linepos++;
   459       FALLTHROUGH;
   460     case '\v':
   461     mb_word_separator:
   462       words += in_word;
   463       in_word = false;
   464       break;
   465     default:
   466       if (wide && iswprint (wide_char))
                  ....
   480       else if (!wide && isprint (to_uchar (*p)))
   481         {
   482           linepos++;
   483           if (isspace (to_uchar (*p)))
   484             goto mb_word_separator;
   485           in_word = true;
   486         }
   487       break;
   488     }
This is a much more complicated implementation than your code. Among other things, note how it uses isprint/iswprint on each character, and how these are locale dependent.

Even in when character = byte, the main loop uses the same logic:

   555     default:
   556       if (isprint (to_uchar (p[-1])))
   557         {
   558           linepos++;
   559           if (isspace (to_uchar (p[-1]))
   560               || isnbspace (to_uchar (p[-1])))
   561             goto word_separator;
   562           in_word = true;
   563         }
   564       break;
   565     }
Your benchmark only uses the characters:

    "\n\r ',-./0123456789ABCDEFGHIJKLMNOPQRSTUVYZabcdefghijklmnopqrstuvwxyz"
which means it comes nowhere near being a good test which verifies the two programs do the same thing.

The following should be a more difficult test set to reproduce wc's output. I create the test set with Python:

    with open("testset.dat", "wb") as f:
      for b1 in range(256):
        for b2 in range(256):
          for b3 in range(256):
            _ = f.write(bytes((b1, b2, b3)))
then print the output using two different locales:

    % env LANG=en_US.UTF-8 wc testset.dat
      196608 1523713 50331648 testset.dat
    % env LANG=C wc testset.dat
      196608 1152001 50331648 testset.dat

Re: Destroying C with 20 lines of Haskell: wc

#62
post #7

But how would Haskell version of wc compare with C version of wc running with LC_ALL=C environment variable? UTF-8 locale is much slower than C locale in coreutils, it's a well-known fact, and their Haskell version of wc is already using fixed 8-bit characters.

wc was actually slower with LC_ALL=C as opposed to ru_RU.UTF-8 that my system normally runs with (about 10 s against 7.2 s). Which actually raises a good question of whether I should have been comparing with that one — but that'd probably raise more questions and lead to more people accusing me of cheating in favour of Haskell.

Well, counting characters is obviously different from counting bytes when the characters are UTF-8 encoded, or UTF-32.

Re: Destroying C with 20 lines of Haskell: wc

#63
post #55

So I just wrote the most naive version of wc I could think of in C, matching the capability of this Haskell version, and I smoked the system wc ... my code, unoptimised, was over twice as fast. $ time wc Backups/Tera2/files.txt 1123699 2283439 161361844 Backups/Tera2/files.txt real 0m2.010s user 0m1.964s sys 0m0.020s $ time naive Backups/Tera2/files.txt L: 1123699 W: 2283439 C: 161361844 real 0m0.864s user 0m0.835s s…

I got 5.6x speedup on just wc alone with: export LANG=C (obviously in both cases with prewarmed filesystem cache)

That doesn't seem to help on macOS. I suspect you're on Linux?

Re: Destroying C with 20 lines of Haskell: wc

#64

Earlier quoted context omitted.

> This doesn't seem to be comparing anything like the same thing. This is a fair point, and I believe this whole series of 'Beating C with foo' posts could have been better named. But I'm of the opinion that the whole series is about showcasing various language's strengths and weaknesses, while using GNU wc as a benchmark. From this perspective, I've learnt a bit about several languages I knew nothing about, so I rat…

It's all about tone. Everyone remembers being a brash know-nothing teenager, so just by the headline "DESTROYING C" you get that vibe. I remember having a blog about Haskell circa 2002 where I would smugly enumerate the ways in which Guido van Rossum was wrong. "GUIDO IS WRONG! PART 4" my post titles would say.

[deleted]

Re: Destroying C with 20 lines of Haskell: wc

#65

> So we’ve managed to just smash a C program that was looked at by thousands of eyes of quite hardcore low-level Unix hackers over a few decades. We did this with a handful of lines of pure, mutation-less, idiomatic Haskell, achieving about 4 to 5 times of throughput of the C version and spending less than an hour on all the optimizations. I've done many very arrogant things in my life, because I've been a strange gu…

Those are fairly trivial and well-known optimizations that I did (and I by no means am an expert in writing high-performant code), so all the honors go to GHC authors.

The github repo description is equally distasteful too:

> wc implemented in Haskell (significantly faster than GNU coreutils version — oops I did it again

For reference, I'm referring to the "oops I did it again" part. It's really hard to take that comment as "honours go to GHC authors".

Also, I suggest you try running the GNU wc with unicode turned off because unicode is computationally expensive and you're deliberately disabling unicode support in your own code anyway. I appreciate you said you'd add in the edge cases that GNU does in your next blog post but disabling unicode in GNU for this benchmark would show good faith that you're at least trying to compare like for like. And if GHC still out performs then you can at least legitimately say:

> My code outperforms GNU for non-unicode strings

Which currently you cannot because your claim is based on incorrect benchmarks.

Re: Destroying C with 20 lines of Haskell: wc

#66
post #61

Earlier quoted context omitted.

> So until you put in locale handling, alternate line endings, option handling, and error handling, I don't see that your post is at all convincing. That's precisely what the second part would be about. And, if I succeed, IMO, that's where Haskell would really shine (because composability and local reasoning), and where I would be able to claim to achieve something — the stuff in the post we're discussing is indeed t…

Here's the source code for GNU wc - https://git.savannah.gnu.org/gitweb/?p=coreutils.git;a=blob;... . It's easy to see that it's not the result of 10+ years of low-level optimizations to eek out the most performance. Your test code probably hits the MB_CUR_MAX>1 path at line 361. (Check your locale setting!) The main loop is: 402 if (!in_shift && is_basic (*p)) 403 { 404 /* Handle most ASCII characters quickly, witho…

This is a great point about handling printable vs non-printable characters that I originally missed when I read wc code. Thank you for pointing this out!

Re: Destroying C with 20 lines of Haskell: wc

#67
post #65

Earlier quoted context omitted.

Those are fairly trivial and well-known optimizations that I did (and I by no means am an expert in writing high-performant code), so all the honors go to GHC authors.

The github repo description is equally distasteful too: > wc implemented in Haskell (significantly faster than GNU coreutils version — oops I did it again For reference, I'm referring to the "oops I did it again" part. It's really hard to take that comment as "honours go to GHC authors". Also, I suggest you try running the GNU wc with unicode turned off because unicode is computationally expensive and you're delibera…

> For reference, I'm referring to the "oops I did it again" part. It's really hard to take that comment as "honours go to GHC authors".

Was overly excited when I created the repo after obtaining the first results. Childish indeed, thanks for reminding, fixed.

> Also, I suggest you try running the GNU wc with unicode turned off because unicode is computationally expensive and you're deliberately disabling unicode support in your own code anyway.

I tried running wc as `LC_ALL=C wc file.txt`, and it (surprisingly for me) resulted in worse run time for wc (my default locale is ru_RU.UTF-8 for comparison). This reproduced on two machines of mine and also on a machine of a friend of mine who also gave my code a shot.

My bad for omitting this in the post, I'll update it accordingly.

Re: Destroying C with 20 lines of Haskell: wc

#68

Earlier quoted context omitted.

Those are fairly trivial and well-known optimizations that I did (and I by no means am an expert in writing high-performant code), so all the honors go to GHC authors.

Thanks for replying. I want to tell you that I feel deep regret for my impulse to publically shame you - even though I've gotten a lot of points for this comment, and did not really receive criticism for it. Hey - if you make performance optimizations and compare implementations, it's probably best not to jump to quick conclusions. I would advise to brush up on C to get a feel for performance. Or, in times where it's…

No worries, that's a natural reaction!

> In practice it's unlikely that you find yourself in a situation where you can write code in a high-level language that runs considerably faster than what you could realistically write in C.

I do way more C++ (in fact, I don't do pure C at all), and aliasing has bitten me and my code performance more often than I'd like. While there are workarounds, I'd probably consider spending time and effort on them as rather unrealistic in a sense. So it surely doesn't contradict my world model if a language with a stricter type system (Haskell? Rust? ATS anyone?) achieves better results on at least some of the tasks with less effort and less dependence on implementation details.

Although ironically I'm going to write something low-level for the Haskell bytestrings library today evening, in C with intrinsics (so almost assembly modulo stuff like register allocation).

Re: Destroying C with 20 lines of Haskell: wc

#69
post #61

Earlier quoted context omitted.

Here's the source code for GNU wc - https://git.savannah.gnu.org/gitweb/?p=coreutils.git;a=blob;... . It's easy to see that it's not the result of 10+ years of low-level optimizations to eek out the most performance. Your test code probably hits the MB_CUR_MAX>1 path at line 361. (Check your locale setting!) The main loop is: 402 if (!in_shift && is_basic (*p)) 403 { 404 /* Handle most ASCII characters quickly, witho…

This is a great point about handling printable vs non-printable characters that I originally missed when I read wc code. Thank you for pointing this out!

Could you elaborate on how you read the wc code?

I ask because the essay and your comments until now show no insight from reading the code.

I find it difficult to understand how anyone could miss that (rather significant) part of the core algorithm, and then assert the differences are due to only "modulo intended Unicode space handling" and the like.

Until now I had assumed you had a lay understanding of wc, and had not read the code.

Re: Destroying C with 20 lines of Haskell: wc

#70
post #69

Earlier quoted context omitted.

This is a great point about handling printable vs non-printable characters that I originally missed when I read wc code. Thank you for pointing this out!

Could you elaborate on how you read the wc code? I ask because the essay and your comments until now show no insight from reading the code. I find it difficult to understand how anyone could miss that (rather significant) part of the core algorithm, and then assert the differences are due to only "modulo intended Unicode space handling" and the like. Until now I had assumed you had a lay understanding of wc, and had…

I was mostly curious about how wc handles spaces and whether ignoring non-ascii spaces brings me closer or farther from what wc does. So I focused on that, and this specific printable characters handling didn't caught my eye.

On a meta level, I wasn't even considering that the notion of a word might be different from "a sequence of characters that aren't space characters".

Live and learn indeed.

Post reply on HN