Shh! Don’t spoil things! Picowatt-hours, I say. Picowatt-hours!
But seriously, although echo is typically a shell built-in and distinctly faster than /usr/bin/echo, it’s still much slower than <<<, presumably because it still has to set up a pipe and an extra… shall we say pseudoprocess.
Comparing behaviours for feeding text into `true` (typically a shell built-in, so that process spawn times doesn’t drown the signal):
try() {
echo -e "\e[32;1m$1\e[m"
for (( run = 0; run < 5; run++ )); do
time (for (( i = 0; i < 1000; i++ )); do
$2
done)
done
}
a() { /usr/bin/echo .dump | true; }; try /usr/bin/echo a
b() { echo .dump | true; }; try echo b
c() { <<<.dump true; }; try '<<<' c
d() { true; }; try "no piping" d
My best times of the five runs, under bash/zsh, expressed in time per iteration:
• /usr/bin/echo: 750μs/845μs
• echo: 469μs/371μs
• <<<: 11μs/31μs
• No piping: 3μs/10μs
So… yeah, on a very slightly older or slower machine than mine, using <<< may save you more than half a millisecond. That’s a much bigger difference than I expected—I was expecting it to be well under 200μs, maybe under 100μs, though the more I think about it the more I realise my expectation may have been unreasonable.
Improved benchmarking script (I just wasn’t thinking carefully at the time, just getting something out quick; but eval is obviously better than requiring a function):
try() {
echo -e "\e[32;1m$1\e[m"
for (( run = 0; run < 5; run++ )); do
time (for (( i = 0; i < 1000; i++ )); do
eval $1
done)
done
}
try "/usr/bin/echo .dump | true"
try "echo .dump | true"
try "<<<.dump true"
try "true"
I also want to note that this is thoroughly into the nanowatt-hour scale (even at the unrealistically low figure of 1W power consumption, 300μs makes it 83⅓ nWh). Tens or hundreds of thousands of picowatt-hours!
There is another difference that users may care about here. zsh will create a temp file for each here-string in the here-string version, and bash may do too¹. Whether those files hit a disk is a matter of system configuration, and whether such a disk is magically quick or swirling rust is a different issue too.
[Just noting that the implementation here isn't equivalent for nerdsnipe-ery, not arguing for real attempts at optimisation of simple pipelines.]
¹ Always with older versions, only for large strings with newer versions.
Running under zsh, link 0 -> '/tmp/zshqqjTS0 (deleted)'; under bash, link 0 -> 'pipe:[1509353]'.
(I know <(…) is substituted in zsh with /proc/self/fd/… for a fd corresponding to a pipe:[…], and regular | piping makes 0 be a pipe:[…].)
I know some common configurations use concrete /tmp. Mine is tmpfs.
Thanks for the info! I like sharing these kinds of things because I have found them interesting and expect a few others will too, and people often add to them details I hadn’t known and like to know!
You can see the cutover on buffer size from my install of bash 5.2.15. Anything under 64k of here-string will still use a pipe.
In case you weren't aware, you can also force a "real" file with zsh process substitution by using =(…)¹. It can be useful when the tool you're using doesn't behave correctly with <(…), if it wishes to seek across it for example. Sadly, =(…) isn't supported in bash [yet?].
¹ In your case it still wouldn't hit a disk as it is in /tmp, but it at least becomes seekable.