Hacker News new | ask | show | jobs
by unscaled 386 days ago
GNU core utils is 134 lines of code, not 50, so the Rust version is even slightly shorter. You can make yes a lot shorter in both C and Rust, but this size goes into speed. For reference, OpenBSD's yes is just 17 lines of code[2]. It essentially boils down to this:

  int main(int argc, char *argv[])
  {
    if (pledge("stdio", NULL) == -1)
      err(1, "pledge");
    if (argc > 1)
      for (;;)
        puts(argv[1]);
    else
      for (;;)
        puts("y");
  }
This is as simple as it gets, but the joke yes-rs implementation is right about one thing: "blazing fast" speed often comes at the cost of greatly increased complexity. The BSD implementation of yes is almost 10 times shorter than the GNU implementation, but the GNU implementation is 100 times faster[3].

[1] https://github.com/coreutils/coreutils/blob/master/src/yes.c

[2] https://github.com/openbsd/src/blob/master/usr.bin/yes/yes.c

[3] https://www.reddit.com/r/unix/comments/6gxduc/how_is_gnu_yes...

4 comments

That reddit thread has some amazing benchmarks.

The GNU-yes

  $ yes | pv > /dev/null
  ... [10.2GiB/s] ...
The way I (not a C programmer) would have written it

  void main() {
      while(write(1, "y\n", 2)); // 1 is stdout
  }

  $ gcc yes.c -o yes
  $ ./yes | pv > /dev/null
  ... [6.21 MiB/s] ...
As a non-system-programmer, here's my attempt in Odin.

  yes | pv > /dev/null
  0:00:15 [1.12GiB/s]
  build/yes | pv > /dev/null
  0:00:20 [1.03GiB/s]


  package main
  
  import "core:sys/linux"
  import "core:os"
  import "core:strings"
  
  main :: proc() {
    msg := "y" if len(os.args) == 1 else os.args[1]
    msg = strings.concatenate({msg, "\n"})
  
    buf := transmute([]u8) strings.repeat(msg, 8192)
    for {
      linux.write(linux.STDOUT_FILENO, buf)
    }
  }
Replace `write(..)` with `puts("y")` and you'll be an order of magnitude faster. This is due to `puts` (`printf` too) being buffered (data isn't written to term/file immediately but retained in memory until some point). Improving this process (as seen in the reddit thread) gets GNU-yes.
It's line buffered when it prints to terminal.
One rarely needs yes' output to be a terminal.
Don't you actively want it to flush asap since you're usually piping into another program?

I suspect what you suggest creates a more voluminous dump but is slower in the desired use case

  yes &
A few times is still my favorite way to push a cpu to max temperature for testing. Used it a lot to detect faulty Core 2 Duo MacBook back in the day. They would short circuit some CPU sensor due to thermal expansion or melting of the wire insulation. Yes was an easy way to get the CPU’s hot enough.
If you compile your variant with -O3 I imagine it will be much faster? Iirc, the default is for GCC is to not optimise
No, it will be about the same. The algorithm is wrong (calling write repeatedly) and -O3 isn't sufficient to rewrite that.
Which implies you get pretty much 3M syscall per second. Which is a good order magnitude to know
I don't believe puts is performing unbuffered I/O though. It's a libc function, not a direct syscall. Correct me if I'm wrong of course
The write(2) libc function is just a C wrapper for the syscall. It's the functions from stdio.h that are buffered.
Ah ok sorry I got confused by the comments nesting level. I thought we were talking about the OpenBSD's version which uses puts
In this case OpenBSD version does a much better job imo (although I don't agree with the lack of braces). The performance of such a tool does not matter at all, and a larger implementation is not only unnecessary, but it can actually introduce bugs in otherwise completely straightforward code
The OpenBSD version of true is also amazing: https://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr...

The GNU version of true/false is more interesting. All the logic is in true and false just redefined the EXIT_STATUS and imports all of true.c. https://github.com/coreutils/coreutils/blob/master/src/false...

So basically they also introduced the complexity of respecting --help and --version.
They did, because it is written somewhere that all GNU programs must conform to having --help and --version. I forgot where I read it.
> This is as simple as it gets

It unnecessarily duplicates the for loop. I would have written something like:

    char *what = argc > 1 ? argv[1] : "y";
    for (;;)
        puts(what);
What a waste of 8 bytes! :)
It’s not about bytes, it’s about duplicating logic that should inherently be the same. If you change something about the loop or the puts, you now have to take care to change it identically in two places to be consistent. That’s a situation that should be avoided, and is what makes it not “as simple as it gets”.
I was being humorous, but tbh it’s not so clear cut!

In 99% of cases, yes of course you’re right, factor this loop.

In this specific case? This is trivial code, that will likely _never_ change. If it does change, it’s extremely unlikely that the two loops would accidentally diverge (the dev would likely not miss one branch, tests would catch it, reviewers would catch it). So if you get any upside by keeping the two loops, it might be worth it.

Here you get 8 bytes back. I honestly can’t see how that would ever matter, but hey it’s _something_, and of course this is a very old program that was running on memory-constrained machines.

So it’s a trade-off of (minor) readability versus (minor) runtime optimisation. I think it’s the better choice (although it’s very minor).

Or maybe there’s a better reason they chose this pattern… can’t imagine the compiler would generate worse code, but maybe it did back in the days?

I agree that it’s borderline pedantic for this simple code, but I also find it an obvious code smell, contradicting the “as simple as it gets”.

If you consistently deduplicate code that is supposed to do the same and evolve the same, then any duplicated code sticks out as a statement of “this isn’t the same”, and in the present case it then makes you wonder what is supposed to be different about both cases. In other words, such code casts doubt on one’s own understanding, raising the question whether one might be overlooking an important conceptual reason for why the code is being kept duplicated. So in that sense I disagree that the duplicated version is more readable, because it immediately raises unanswered questions.

About possible performance reasons, those need an explanatory comment, exactly for the above reason. And also, if performance reasons warrant complicating the code, then it isn’t “as simple as it gets” any more. I was commenting because I disagreed with that latter characterization.

I agree it's a code _smell_. But a "smell" doesn't mean that something is necessarily wrong, just that there's a clue that it might be wrong.

> in that sense I disagree that the duplicated version is more readable

I didn't say it is, I agreed it's _less_ readable. I said it's trading off readability for 8 bytes of memory at runtime.

> If you consistently deduplicate code that is supposed to do the same and evolve the same, then [...]

I agree with all this. I'm not saying to consistently go for the deduplicated approach (I don't think anyone would say that), I'm saying it's a reasonable trade-off in this specific case (each branch is still trivial, and the code won't evolve much if at all).

> About possible performance reasons, those need an explanatory comment, exactly for the above reason.

Agreed.

> if performance reasons warrant complicating the code, then it isn’t “as simple as it gets” any more. I was commenting because I disagreed with that latter characterization.

Also agreed.

To some, the current way is more readable than yours, though. It is much more explicit, and that often is a good thing.