Live data from Hacker News

Wc in D: 712 Characters Without a Single Branch

dlang.org

61–70 of 89 posts

Re: Wc in D: 712 Characters Without a Single Branch

#61
post #14

Perhaps I'm just being pedantic or maybe I am misunderstanding. The author claims to be IO bound toward the end. But they are comparing to two versions that are faster. It is my understanding that IO-bound means that the IO subsystem is the thing which limits run time of the program. But the author clearly demonstrates that the IO subsystem of their machine is capable of supporting faster wc binaries. So what am I mi…

Maybe the author considers "IO bound" to include "Uses IO inefficiently". If a program uses unbuffered IO one character at a time, it does spend almost all its runtime waiting on IO syscalls.

Re: Wc in D: 712 Characters Without a Single Branch

#62

I'm wavering between D and Rust for which one could be used as a better alternative to C++. (Though I don't see C++ getting replaced anytime soon.) Even if Rust is getting most of the attention, D also seems to be a strong candidate.

I'd say the main determinant is what kind of C++ are you writing and what features of C++ are most important for you. If template metaprogramming is your kind of thing, then you absolutely need to try D, D makes heavy use of template metaprogramming (yes, the lengthy errors you know from C++ make a comeback in D too). If your C++ is more like C with classes, you'll feel right at home in D too.

However, if the most important features of C++ is RAII, shared pointers, unique pointers, move semantics, then you'd probably enjoy Rust more, because it focuses on this kind of memory/resource management techniques.

Re: Wc in D: 712 Characters Without a Single Branch

#63
post #14

Perhaps I'm just being pedantic or maybe I am misunderstanding. The author claims to be IO bound toward the end. But they are comparing to two versions that are faster. It is my understanding that IO-bound means that the IO subsystem is the thing which limits run time of the program. But the author clearly demonstrates that the IO subsystem of their machine is capable of supporting faster wc binaries. So what am I mi…

I'm confused by this as well. A while ago I needed to count the number of lines in an ununiformly long text file. I was initially using wc but that got too slow so I wrote something that was like wc but only did line counting. I polished it up during a flight to a con and published it on my "blog" [0]. I thought I'd benchmark it to see if these versions beat hacky and gross my C implementation. They don't seem to do so.

My implementation was very close to what I calculated for my hardware's max throughput within 20x the runtime of doing a direct file copy.

For my test file I'm using a copy of the linux source tree in a singe file generated with: `(find linux/ -type f -name ".c" && find linux/ -type f -name ".h") | xargs cat > linux.txt` which is about 770MB of text.

To get an idea of the maximum possible performance I could hope to achieve:

    cat linux.txt | pv > /dev/null
    769MiB 0:00:00 [3.08GiB/s] [                                                                                                                                                                        
 ]
Some things I noted when doing my testing is that each program counted the number of lines, characters, and words differently in this corpus. I know for a fact that my program's line count is the only thing I tested at all and I'm assuming `wc` from gnu is well tested. Each program returned consistent results. My guess is there's some control/utf8 character or something in the source that isn't playing nice with everything.

First the haskell implementation that uses optics, concurrency, and other magic:

    $ nproc 
    32
    $ time ./hs-wc lazy linux.txt
    24961824 77271007 807304327 linux.txt
    ./hs-wc lazy linux.txt  6.88s user 0.89s system 317% cpu 2.446 total
    $ time ./hs-wc simple linux.txt
    24961824 77270977 807299417 linux.txt
    ./hs-wc simple linux.txt  509.04s user 432.26s system 1850% cpu 50.867 total

Then the D implementation from this post:

    $ time ./d-wc linux.txt 
    24961824 77270980 807299417 linux.txt
    ./d-wc linux.txt  22.09s user 0.14s system 99% cpu 22.238 total
The implementation that comes with ubuntu 19.10:

    $ time wc linux.txt 
    24961824  77270960 807304327 linux.txt
    wc linux.txt  3.59s user 0.08s system 99% cpu 3.672 total
And finally my simple implementation in C:

    $ time ./mine-wc linux.txt 
    24961824 77270966 807304327 linux.txt
    ./mine-wc linux.txt  1.70s user 0.10s system 99% cpu 1.804 total

    $ gcc -O0 wc.c          
    $ time ./a.out linux.txt
    24961824 77270966 807304327 linux.txt
    ./a.out linux.txt  6.51s user 0.12s system 99% cpu 6.636 total

    $ gcc -Wall -Isrc/ -pedantic-errors -Ofast -ftree-vectorize -msse -msse2 -ffast-math wc.c 
    $ time ./a.out linux.txt
    24961824 77270966 807304327 linux.txt
    ./a.out linux.txt  1.37s user 0.09s system 99% cpu 1.467 total

I might be missing something but from my understanding these are not yet bound by my system IO. It might be in the authors use cases however since each disk, system config, etc is different.

[0] - https://closedjdk.com/post/why-is-wc-so-slow/

Re: Wc in D: 712 Characters Without a Single Branch

#65
post #57
post #47

I believe I'm missing something here or my day was too long, but in Clojure this can be squeezed in 13 lines and 435 characters keeping things fairly readable (for Clojure & Lisp developers ;)). (defn wc [^String file] (with-open [rdr (clojure.java.io/reader file)] (apply (partial printf "%d %d %d\n") (reduce (fn [[nl nw nb] ^String ln] (let [words (count (.split ln "[ ]+")) bytes (alength (.getBytes ln "UTF-8"))] [(…

Neat! I wonder if the character decoding & regex usage has noticeable performance impact. My Common Lisp version was sped up somewhat by switching from a character stream to a byte stream: https://git.tazj.in/tree/fun/wcl/wc.lisp You can try this one via Nix with: nix-build -E '(import (builtins.fetchGit "https://git.tazj.in") {}).fun.wcl'

It's been a while since I last wrote CL but I think your program can produce any counts between zero and the correct one – you need to use eql instead of eq if you want this to work in standard common lisp.

Re: Wc in D: 712 Characters Without a Single Branch

#66
post #14

Perhaps I'm just being pedantic or maybe I am misunderstanding. The author claims to be IO bound toward the end. But they are comparing to two versions that are faster. It is my understanding that IO-bound means that the IO subsystem is the thing which limits run time of the program. But the author clearly demonstrates that the IO subsystem of their machine is capable of supporting faster wc binaries. So what am I mi…

I'm confused by this as well. A while ago I needed to count the number of lines in an ununiformly long text file. I was initially using wc but that got too slow so I wrote something that was like wc but only did line counting. I polished it up during a flight to a con and published it on my "blog" [0]. I thought I'd benchmark it to see if these versions beat hacky and gross my C implementation. They don't seem to do…

>I was initially using wc but that got too slow so I wrote something that was like wc but only did line counting.

Any reasonable wc should be fast enough for that purpose; just remember to use the '-l' switch to activate the fast path for line counting.

>Some things I noted when doing my testing is that each program counted the number of lines, characters, and words differently in this corpus.

All programs should report the same line counts (should be exactly the number of line feed characters in the input).

Other than that, it really depends on your current locale and the wc you're using (see https://github.com/expr-fi/fastlwc README for more details). Do note that this D implementation seems to implement the wc character counting behaviour you get with the '-m' switch (instead of the byte counting default).

Also worth noting that different operating systems have different locale definitions; glibc locales explicitly treat non-breaking spaces as non-whitespace characters, while for example Windows doesn't.

Re: Wc in D: 712 Characters Without a Single Branch

#67
post #14

Perhaps I'm just being pedantic or maybe I am misunderstanding. The author claims to be IO bound toward the end. But they are comparing to two versions that are faster. It is my understanding that IO-bound means that the IO subsystem is the thing which limits run time of the program. But the author clearly demonstrates that the IO subsystem of their machine is capable of supporting faster wc binaries. So what am I mi…

> So what am I missing here? The Haskell program is multi-threaded (I don't know about wc's official implementation, but I assume it is as well), while the D program is single-threaded.

I saw that. If adding threads makes a program faster, that implies that more CPU can make it go faster. If the filesystem is already streaming bytes to the program as fast as it can, adding threads just means that you have more threads waiting for bytes.

If adding threads increases the speed, this implies that the program is both CPU-bound and that it has opportunities for parallelism.

Re: Wc in D: 712 Characters Without a Single Branch

#68
post #65
post #57

Earlier quoted context omitted.

Neat! I wonder if the character decoding & regex usage has noticeable performance impact. My Common Lisp version was sped up somewhat by switching from a character stream to a byte stream: https://git.tazj.in/tree/fun/wcl/wc.lisp You can try this one via Nix with: nix-build -E '(import (builtins.fetchGit "https://git.tazj.in") {}).fun.wcl'

It's been a while since I last wrote CL but I think your program can produce any counts between zero and the correct one – you need to use eql instead of eq if you want this to work in standard common lisp.

Ah, you're right of course - fixed. Though that still won't make this portable, as I'm using an SBCL-specific way of accessing argv.

Re: Wc in D: 712 Characters Without a Single Branch

#70
post #47

I believe I'm missing something here or my day was too long, but in Clojure this can be squeezed in 13 lines and 435 characters keeping things fairly readable (for Clojure & Lisp developers ;)). (defn wc [^String file] (with-open [rdr (clojure.java.io/reader file)] (apply (partial printf "%d %d %d\n") (reduce (fn [[nl nw nb] ^String ln] (let [words (count (.split ln "[ ]+")) bytes (alength (.getBytes ln "UTF-8"))] [(…

Since we're golfing, here's a 51-character version in fairly readable J:

    wc=:[:+/[:(1:,([:#'\S+'&rxmatches),>:@#);._2 freads
I'm guessing that a real J wizard could squeeze out a few more characters. :)
Post reply on HN