[HN Gopher] How efficient can cat(1) be?
       ___________________________________________________________________
        
       How efficient can cat(1) be?
        
       Author : benhoyt
       Score  : 71 points
       Date   : 2022-07-18 06:42 UTC (16 hours ago)
        
 (HTM) web link (ariadne.space)
 (TXT) w3m dump (ariadne.space)
        
       | blibble wrote:
       | how about ptracing into your target process and dup2()'ing your
       | FD across?
       | 
       | infinitely fast cat
        
       | DannyBee wrote:
       | While it's certainly just example code, the initial sendfile
       | version is badly buggy.                  /* Fall back to
       | traditional copy if the spliced version fails. */         if
       | (!spliced_copy(srcfd))          copy(srcfd);
       | 
       | The thing that they are trying to avoid is sendfile failing due
       | to inability to mmap the the fd. But they don't check for that
       | (it would return EINVAL), and in fact, by converting the error
       | code to boolean, destroy the ability to differentiate[1].
       | Instead, they check that sendfile failed for _any_ reason, and
       | then redo it with copy.
       | 
       | Which means sendfile could output half the data, fail for some
       | reason, and depending on why it failed, the fallback copy
       | read/write will do bad things. for example, output the same data
       | again, or more likely, skip data. Since they are just reading
       | from the fd as it now exists after sendfile failing, it is most
       | likely to skip data but pretend it completed successfully.
       | 
       | Normally, cat would just fail in that situation, as this should -
       | it should not retry the copy when sendfile returns EINVAL or
       | ENOSYS
       | 
       | This is what you get for transforming error codes into booleans
       | :)
       | 
       | (I guess errno will still be set, and they could still check it
       | here, but ugh)
       | 
       | [1] This is why the man page says: Applications may wish to fall
       | back to read(2)/write(2) in the case where sendfile() fails with
       | EINVAL or ENOSYS.
        
         | Joker_vD wrote:
         | Should sendfile(2) _not_ report error if it managed to send a
         | non-zero amount of data? IIRC write(2) behaves like that.
        
           | DannyBee wrote:
           | I don't honestly remember all of the write semantics - I
           | thought it will only not report error if it gets interrupted
           | by a signal handler after it wrote something. In case of true
           | error, i thought it always return an error.
           | 
           | write _does_ have a weird error-checking semantic - you can
           | get it to check for a bunch of errors without writing data by
           | using a count of 0.
           | 
           | At least as documented, sendfile does not have any of these
           | semantics - it only returns number of bytes written if the
           | transfer was successful. Otherwise, you can't tell how far it
           | made it :)
        
             | saurik wrote:
             | As documented, sendfile (I haven't looked at whatever this
             | "spliced_copy" is) does what Joker_vD said it should: if it
             | does a partial write then it is considered "successful" and
             | the caller is required to retry the call if less than the
             | expected amount of data was sent.
        
       | digitalsushi wrote:
       | From my experience, it will never be so efficient that someone
       | smarter than me doesn't publicly shame me for winning the
       | "useless use of cat" award on a forum where I ask for help.
       | 
       | 28 years later and I'm still sore I asked for help as a 15 year
       | old that one time. Very effective way to teach a new user.
        
         | [deleted]
        
         | kccqzy wrote:
         | Another reason I still use "cat" is that I don't want the
         | remaining commands to modify the input file, but I don't want
         | to spend the time to inspect the command line to make sure that
         | is the case.
         | 
         | For example I'm pretty sure "grep" won't change the input file
         | but "sed" may depending on the "-i" flag. Using "cat"
         | conveniently bypasses all that thinking because the program
         | only gets stdin from a pipe.
        
         | Myrmornis wrote:
         | that's a lesson that's no longer applicable on modern machines
         | I think? --- it often makes sense to start a sequence of piped
         | commands with a cat invocation. One reason is that then the
         | order of sources and sinks in the command syntax matches
         | reality.
        
           | samatman wrote:
           | I always pipe from cat, as a matter of habit.
           | 
           | Reason being: I've probably just catted the file at least
           | once, and it's even likely I catted it right before piping
           | it. So I can extend that command from the history, and if I
           | keep piping (likely) I don't have the weird syntactic stutter
           | at the beginning where `blah < file.txt` | next` puts things
           | out of order.
           | 
           | Also, you can't mistype `cat file.txt | blah` and overwrite
           | file.txt accidentally with the output of blah. That's
           | ergonomic.
        
       | ttgurney wrote:
       | Tangent: It frustrates me that it's apparently impossible to
       | implement cat(1) in a truly portable way.
       | 
       | The problem is supporting unbuffered I/O (`cat -u`). Standard C
       | simply can't do it. setvbuf(3) allows you to change the buffering
       | on an I/O stream, but then fread(3) only allows you to read
       | exact-sized blocks of data. You can only get a short read on EOF
       | or error. So there is no way to say "give me as much data as is
       | available, up to X amount of bytes" and therefore no way to
       | implement unbuffered cat(1) efficiently using only ISO C. You
       | need POSIX for that.
        
       | dochtman wrote:
       | I'm wondering if it would make sense to use io_uring for this
       | kind of thing. If not, why not?
        
       | bear8642 wrote:
       | remember enjoying reading the simple plan9 version of cat(1) -
       | <http://9p.io/sources/plan9/sys/src/cmd/cat.c>
        
       | ape4 wrote:
       | It would be nice if user programs didn't have to jump through
       | loops like this. It would be ideal if the kernel made the naive
       | implementation work efficiently.
        
         | OskarS wrote:
         | How would you suggest the kernel would accomplish that feat? If
         | the user calls read() on a file descriptor, what can the kernel
         | do except... you know... actually read from it and copy the
         | data to user space?
        
           | ape4 wrote:
           | I know it wouldn't be easy. But the goal could be to let
           | users write simple programs that say what they mean. Since
           | many programs do: while(!eof) { read(); write(); } that could
           | be optimized similar to the way a compiler optimizes known
           | idioms. Perhaps it could notice a well know sequence of calls
           | and avoid userspace/kernels space copies. But, yeah, very
           | non-trivial.
        
           | Someone wrote:
           | It _could_ look at the instructions following the _read_ call
           | and do a sort of high-level software macro-op fusion
           | (https://en.wikichip.org/wiki/macro-operation_fusion)
           | 
           | That already would be going beyond the duty for a jitting
           | virtual machine, so I don't think you can expect that from a
           | CPU. I also likely would make lots of programmers uneasy if
           | their OS does that sort of thing.
           | 
           | For a 'real' CPU, I also fear handling all the edge cases
           | would be horrific (the program may do a _read-write_ pair of
           | calls, but how do you ascertain it doesn't read that data
           | later? What if the read call tries to read into unwritable
           | memory? What if the program is being traced, and _read_ calls
           | are being logged? Etc)
        
       | rstarast wrote:
       | This was a good read! The missing hyperlinks:
       | 
       | - https://man7.org/linux/man-pages/man2/sendfile.2.html
       | 
       | - https://man7.org/linux/man-pages/man2/splice.2.html
       | 
       | (Funny how used I've gotten to "hypertext", I was quite irritated
       | I couldn't click those function names.)
        
         | kevin_thibedeau wrote:
         | Pinfo makes regular man pages navigable.
        
           | rascul wrote:
           | And makes info pages pleasant to view.
        
           | CoastalCoder wrote:
           | I'm having a _great_ Linux day! TIL about  'splice' _and_
           | 'pinfo'. Thanks!
        
             | pkilgore wrote:
             | Go subscribe to https://lwn.net/ and have these moments
             | every day!
        
         | jwilk wrote:
         | Also: https://man7.org/linux/man-
         | pages/man2/copy_file_range.2.html
        
       | formerly_proven wrote:
       | copy_file_range is much preferable to any of these because
       | filesystems _can_ "hook into" it and just share the underlying
       | data, not copying at all.
        
         | Denvercoder9 wrote:
         | In general, yes, but as the article notes, itt's not applicable
         | to cat, as cat always outputs to stdout and not to a file.
        
           | formerly_proven wrote:
           | You can handle the cat foo bar baz > file case this way
           | though, because stdout _is_ a file then.
        
             | ariadneconill wrote:
             | that is why the article mentions it as an honorable mention
             | :P
        
       | jstimpfle wrote:
       | A bit of a tangent, there are few instances of "do-while" that
       | I've ever ever written and not removed soon after. In practice,
       | I've found that the looping situations that don't easily match
       | the "for (int i = 0; i < n; i++)" pattern are normally "random"
       | enough that it's best to just write "for (;;)" and put explicit
       | checks and breaks inside the body, wherever they naturally fit.
       | Forcing "for (...)" or "while (...)" or "do-while(...)" syntactic
       | constructs is likely to lead to an unnatural sequence of
       | statements. Doing break anywhere is just fine!
       | do         {             splice(from stdin...);
       | if (A)                 handle_a();                 goto out;
       | splice(to stdout...);                  if (B)
       | handle_b();                 goto out;              } while
       | (nwritten > 0);             // do we need some kind of
       | handle_c()??              out:             ...
       | 
       | Why make a special case for the "nwritten > 0" condition here?
       | And what's wrong with "break" vs "goto"?                   for
       | (;;)         {             thing_a();                  if (A)
       | handle_a();                 break;                  thing_b();
       | if (B)                 handle_b();                 break;
       | if (C)                 handle_c();                 break;
       | }
       | 
       | is cleaner in my eyes.
        
         | faho wrote:
         | First of all: Your code is missing braces around the `if`
         | blocks - the `goto out` would be run unconditionally.
         | 
         | But anyway, the case for `goto` here is that it jumps
         | immediately to the cleanup that needs to happen always.
         | 
         | If you put something between the loop and that, `break` would
         | jump before that and also execute that.
         | 
         | Yes, this is a workaround for C's lack of automatic cleanup
         | (RAII, garbage collection, python's `with` or whatever).
        
           | jstimpfle wrote:
           | > Your code is missing braces around the `if` blocks
           | 
           | Sure. It's obviously a sketch.
           | 
           | > If you put something between the loop and that, `break`
           | would jump before that and also execute that.
           | 
           | Sure. But I rarely can see a need to make it so complicated
           | (not in this case anyway). If you need multiple distinct
           | cleanup sections that can't be inlined, then label them all
           | (or put them in separate procedures) and jump to them
           | explicitly. In my example, there is no need for any labels at
           | all.
           | 
           | > this is a workaround for C's lack of automatic cleanup
           | (RAII, garbage collection, python's `with` or whatever).
           | 
           | No need for workarounds here.
        
         | sharikous wrote:
         | "do ... while" maps neatly into Assembly for me. I think it is
         | meant as a bridge between low level thinking and structured
         | programming.
        
       | pif wrote:
       | Slightly out of topic:
       | 
       | > There have been a few initiatives in recent years to implement
       | new a new userspace base system for Linux distributions as an
       | alternative to the GNU coreutils and BusyBox.
       | 
       | Have there been? And why?
        
         | ttgurney wrote:
         | > Have there been? And why?
         | 
         | A few reasons why people might want to do this:
         | 
         | - Optimizing for small approachable codebase instead of
         | featurefulness or performance (sbase)[1]
         | 
         | - Dissatisfaction with GPL (toybox)[2]
         | 
         | - Desire to replace C (described as an "unsafe" language) with
         | Rust or Go (examples exist but I don't know of specific ones)
         | 
         | [1]: https://core.suckless.org/sbase/
         | 
         | [2]: https://landley.net/toybox/
        
         | jwilk wrote:
         | I heard only about this one:
         | 
         | https://github.com/uutils/coreutils ("Cross-platform Rust
         | rewrite of the GNU coreutils")
        
         | odc wrote:
         | Why indeed! Recent versions of GNU cat (and other coreutils)
         | include those optimizations.
         | 
         | https://git.savannah.gnu.org/cgit/coreutils.git/tree/src/cat...
        
           | jwilk wrote:
           | To be honest, GNU coreutils is not a good alternative to GNU
           | coreutils. :-P
        
       | eurasiantiger wrote:
       | cat_spew()
       | 
       | Eww.
        
       | robertlagrant wrote:
       | I couldn't find the bit where the original performance claim was
       | refuted (or not). Was it one of the listed options?
        
         | faho wrote:
         | The original performance claim was about
         | https://vimuser.org/cat.c.txt.
         | 
         | Which just does read/write - so it's the same as the "cat-
         | simple" example, which is the slowest listed.
         | 
         | GNU cat [0] does copy_file_range if it can and falls back to a
         | read/write loop otherwise, so it's unlikely to be much slower
         | (possibly some overhead from argument parsing, but that's just
         | a constant).
         | 
         | So the performance claims are wrong.
         | 
         | [0]:
         | https://git.savannah.gnu.org/cgit/coreutils.git/tree/src/cat...
        
       ___________________________________________________________________
       (page generated 2022-07-18 23:02 UTC)