I've done the following test and on my system the resulting difference is about 100 times longer for the second script.
My file is a strace output called bigfile
$ wc -l bigfile.log
1617000 bigfile.log
Scripts
xtian@clafujiu:~/tmp$ cat p1.sh
tail -n 1000000 bigfile.log | grep '"success": true' | wc -l
tail -n 1000000 bigfile.log | grep '"success": false' | wc -l
xtian@clafujiu:~/tmp$ cat p2.sh
log=$(tail -n 1000000 bigfile.log)
echo "$log" | grep '"success": true' | wc -l
echo "$log" | grep '"success": true' | wc -l
I don't actually have any matches for the grep so nothing is written to the last pipe through to wc -l
Here are the timings:
xtian@clafujiu:~/tmp$ time bash p1.sh
0
0
real 0m0.381s
user 0m0.248s
sys 0m0.280s
xtian@clafujiu:~/tmp$ time bash p2.sh
0
0
real 0m46.060s
user 0m43.903s
sys 0m2.176s
So I ran the two scripts again via the strace command
strace -cfo p1.strace bash p1.sh
strace -cfo p2.strace bash p2.sh
Here are the results from the traces:
$ cat p1.strace
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
97.24 0.508109 63514 8 2 waitpid
1.61 0.008388 0 84569 read
1.08 0.005659 0 42448 write
0.06 0.000328 0 21233 _llseek
0.00 0.000024 0 204 146 stat64
0.00 0.000017 0 137 fstat64
0.00 0.000000 0 283 149 open
0.00 0.000000 0 180 8 close
...
0.00 0.000000 0 162 mmap2
0.00 0.000000 0 29 getuid32
0.00 0.000000 0 29 getgid32
0.00 0.000000 0 29 geteuid32
0.00 0.000000 0 29 getegid32
0.00 0.000000 0 3 1 fcntl64
0.00 0.000000 0 7 set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00 0.522525 149618 332 total
And p2.strace
$ cat p2.strace
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
75.27 1.336886 133689 10 3 waitpid
13.36 0.237266 11 21231 write
4.65 0.082527 1115 74 brk
2.48 0.044000 7333 6 execve
2.31 0.040998 5857 7 clone
1.91 0.033965 0 705681 read
0.02 0.000376 0 10619 _llseek
0.00 0.000000 0 248 132 open
...
0.00 0.000000 0 141 mmap2
0.00 0.000000 0 176 126 stat64
0.00 0.000000 0 118 fstat64
0.00 0.000000 0 25 getuid32
0.00 0.000000 0 25 getgid32
0.00 0.000000 0 25 geteuid32
0.00 0.000000 0 25 getegid32
0.00 0.000000 0 3 1 fcntl64
0.00 0.000000 0 6 set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00 1.776018 738827 293 total
Analysis
Not surprisingly, in both cases most of the time is spent waiting for a process to complete, but p2 waits 2.63 times longer than p1, and as others have mentioned, you are starting late in p2.sh.
So now forget about the waitpid
, ignore the %
column and look at the seconds column on both traces.
Largest time p1 spends most of its time in read probably understandably, because there's a large file to read, but p2 spends 28.82 times longer in read than p1 does. - bash
is not expecting to read such a large file into a variable and is probably reading buffer at a time, splitting into lines and and then getting another.
read count p2 is 705k vs 84k for p1, each read requiring a context switch into kernel space and out again. Nearly 10 times the number of reads and context switches.
Time in write p2 spends 41.93 times longer in write than p1
write count p1 does more writes than p2, 42k vs 21k, however they are much faster.
Probably because of the echo
of lines into grep
as opposed to tail writing buffers.
Further more, p2 spends more time in write than it does in read, p1 is the other way round!
Other factor Look at the number of brk
system calls : p2 spends 2.42 times longer breaking than it does reading! In p1 (it doesn't even register). brk
is when the program needs to expand its address space because enough wasn't allocated initially, this is probably due to bash having to read that file into the variable, and not expecting it to be that large, and as @scai mentioned, if the file gets too large, even that would not work.
tail
is probably quite an efficient file reader, because this is what it was designed to do, it probably memmaps the file and scans for line breaks, thus allowing the kernel to optimise the i/o. bash doesn't isn't as good both on time spent reading and writing.
p2 spends 44ms and 41ms in clone
and execv
it's not a measurable amount for p1. Probably bash reading and creating the variable from tail.
Finally the Totals p1 executes ~ 150k system calls vs p2 740k (4.93 times greater).
Eliminating waitpid, p1 spends 0.014416 seconds executing system calls, p2 0.439132 seconds (30 times longer).
So it appears p2 spends most of the time in user space doing nothing except waiting for the system calls to complete and the kernel to regorganise memory, p1 performs more writes, but is more efficient and causes significantly less system load, and is therefore faster.
Conclusion
I would never try to worry about coding through memory when writing a bash script, that does not mean to say that you do not try to be efficient.
tail
is designed to do what it does, it probably memory maps
the file so that it is efficient to read and allows the kernel to optimise the i/o.
A better way to optimise your problem might be to first grep
for '"success": ' lines and then count the trues and falses, grep
has a count option which again avoids the wc -l
, or even better still, pipe the tail through to awk
and count trues and falses concurrently. p2 not only takes long but adds load to the system whilst memory is being shuffled about with brks.
$( command substitution )
is not streamed. All of the rest happens through pipes concurrently, but in the second example you have to wait for thelog=
to complete. Try it with <<HERE\n${log=$(command)}\nHERE - see what you get.grep
for, you might see some speedup usingtee
so the file is definitely only read once.cat stdout.log | tee >/dev/null >(grep -c 'true'>true.cnt) >(grep -c 'false'>false.cnt); cat true.cnt; cat false.cnt
tail -n 10000 | fgrep -c '"success": true'
and false.