In a nutshell:
yes
exhibits similar behavior to most other standard utilities which typically write to a FILE STREAM with output buffered by the libC via stdio
. These only do the syscall write()
every some 4kb (16kb or 64kb) or whatever the output block BUFSIZ
is . echo
is a write()
per GNU
. That's a lot of mode-switching (which is not, apparently, as costly as a context-switch).
And that's not at all to mention that, besides its initial optimization loop, yes
is a very simple, tiny, compiled C loop and your shell loop is in no way comparable to a compiler optimized program.
But I was wrong:
When I said before that yes
used stdio
, I only assumed it did because it behaves a lot like those that do. This was not correct - it only emulates their behavior in this way. What it actually does is very like an analog to the thing I did below with the shell: it first loops to conflate its arguments (or y
if none) until they might grow no more without exceeding BUFSIZ
.
A comment from the source immediately preceding the relevant for
loop states:
/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item. */
yes
does its own write()
s thereafter.
Digression:
(As originally included in the question and retained for context to a possibly informative explanation already written here):
I've tried timeout 1 $(while true; do echo "GNU">>file2; done;)
but unable to stop loop.
The timeout
problem you have with the command substitution - I think I get it now and can explain why it doesn't stop. timeout
doesn't start because its command-line is never run. Your shell forks a child shell, opens a pipe on its stdout and reads it. It will stop reading when the child quits, and then it will interpret all the child wrote for $IFS
mangling and glob expansions, and with the results, it will replace everything from $(
to the matching )
.
But if the child is an endless loop that never writes to the pipe, then the child never stops looping, and timeout
's command-line is never completed before (as I guess) you do Ctrl+C and kill the child loop. So timeout
can never kill the loop which needs to complete before it can start.
Other timeout
s:
... simply aren't as relevant to your performance issues as the amount of time your shell program must spend switching between user- and kernel-mode to handle output. timeout
, though, is not as flexible as a shell might be for this purpose: where shells excel is in their ability to mangle arguments and manage other processes.
As is noted elsewhere, simply moving your [fd-num] >> named_file
redirection to the loop's output target rather than only directing output there for the command looped over can substantially improve performance because that way at least the open()
syscall need only be done the once. This also is done below with the |
pipe targeted as output for the inner loops.
Direct comparison:
You might do like:
for cmd in exec\ yes 'while echo y; do :; done'
do set +m
sh -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
set -m
done
256659456
505401
Which is kind of like the command sub relationship described before, but there's no pipe and the child is backgrounded until it kills the parent. In the yes
case the parent has actually been replaced since the child was spawned, but the shell calls yes
by overlaying its own process with the new one and so the PID remains the same and its zombie child still knows who to kill after all.
Bigger buffer:
Now let's see about increasing the shell's write()
buffer.
IFS="
"; set y "" ### sets up the macro expansion
until [ "${512+1}" ] ### gather at least 512 args
do set "$@$@";done ### exponentially expands "$@"
printf %s "$*"| wc -c ### 1 write of 512 concatenated "y\n"'s
1024
I chose that number because output strings any longer than 1kb were getting split out into separate write()
's for me. And so here's the loop again:
for cmd in 'exec yes' \
'until [ "${512+:}" ]; do set "$@$@"; done
while printf %s "$*"; do :; done'
do set +m
sh -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
set -m
done
268627968
15850496
That's 300 times the amount of data written by the shell in the same amount of time for this test than the last. Not too shabby. But it's not yes
.
Felated:
As requested, there is a more thorough description than the mere code comments on what is done here at this link.
date
is somewhat heavy-weight, plus the shell has to re-open the output stream forecho
for every loop iteration. In the first example, there is only a single command invocation with a single output redirection, and the command is extremely lightweight. The two are in no way comparable.date
may be heavy-weight, see edit to my question.timeout 1 $(while true; do echo "GNU">>file2; done;)
is the wrong way to usetimeout
since thetimeout
command will only start once the command substitution is finished. Usetimeout 1 sh -c 'while true; do echo "GNU">>file2; done'
.write(2)
system calls, not on boatloads of other syscalls, shell overhead, or even process creation in your very first example (which runs and waits fordate
for every line printed to the file). One second of writing is barely enough for it to bottleneck on disk I/O (rather than CPU/memory), on a modern system with lots of RAM. If allowed to run longer, the difference would be smaller. (Depending on how bad a bash implementation you use, and relative speed of CPU and disk, you might not even saturate disk I/O with bash).