4

The following command takes about 10 minutes to output the result

find . -name "muc*_*_20160920_*.unl*" | xargs zcat |
    awk -F "|" '{if($14=="20160920100643" && $22=="567094398953") print $0}'| head

How can I improve its performance?

6
  • 1
    Can you use locate instead of find ? And If you want to stick with find then also use exec rather piping it to xargs
    – SHW
    Commented Sep 22, 2016 at 7:38
  • 2
    The answer depends on how many files there are, how they are distributed under ., and how large they are. Ten minutes could be a perfectly reasonable time. Commented Sep 22, 2016 at 7:45
  • Try sorting the input on column 14 before piping to awk
    – user14755
    Commented Sep 22, 2016 at 7:48
  • 1
    If you need just 10 first line (head) to speed up process do counting inside awk: if(++count == 10) exit but much better use zgrep instead all pipes.
    – Costas
    Commented Sep 22, 2016 at 8:11
  • @SHW xargs should be faster than -exec because it starts only one zcat process for many files. BTW xargs --max-procs=4 could run it in parall.
    – rudimeier
    Commented Sep 22, 2016 at 10:09

4 Answers 4

4

That's already quite optimised. It's hard to know what the bottle neck is without knowing more details like:

  • type of storage (HD, SSD, network, RAIDed)
  • number and average size of matching files
  • number of directories and other non-matching files
  • number of fields in each line
  • average length of a line

Things you can do in any case:

  • replace -print | xargs with -exec cmd {} + or -print0 | xargs -r0 if your find/xargs support it. -print | xargs is not only wrong but also more expensive as xargs needs to decode characters to find out which ones are blanks and do some expensive quote processing.
  • fix the locale to C (export LC_ALL=C). Since all the characters involved here (| and decimal digits for the file contents and latin letters, period and underscore for the file names) are part of the portable charset, if your charset is otherwise UTF-8 or some other multi-byte charset, switching to C with its single-byte charset will safe a lot of work for find and awk.
  • simplify the awk part to: awk -F "|" '$14 == "20160920100643" && $22 == "567094398953"'.
  • since you're piping the output to head, you may want to disable output buffering for awk so that it outputs those 10 lines as early as possible. With gawk or mawk, you can use fflush() for that. Or you could add a if (++n == 10) exit in awk.

To sum-up:

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -exec zcat {} + |
  awk -F "|" '$14 == "20160920100643" && $22 == "567094398953" {
    print; if (++n == 10) exit}')

If CPU is the bottle-neck, on a multi-core GNU system, you could try:

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -print0 |
  xargs -r0P 4 -n 100 sh -c '
    zcat "$@" | 
      awk -F "|" "\$14 == "20160920100643" && \$22 == "567094398953" {
        print; fflush()}"' sh | head)

To run 4 zcat | awk jobs in parallel on 100 files batches.

If that 20160920100643 is a time-stamp, you may want to exclude files that have been last-modified before that. With GNU or BSD find, add a -newermt '2016-09-20 10:06:42'.

If lines have a great number of fields, you get a penalty for awk splitting it and allocating so many $n fields. Using an approach that only considers the first 22 fields could speed things up:

grep -E '^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)'

instead of the awk command. With GNU grep, add the --line-buffered option to output the lines as early as possible in the parallel approach or -m 10 to stop after 10 matches in the non-parallel one.

To sum up, if CPU is the bottle neck and you have at least 4 CPU cores on your system and there are at least 400 muc* files and you're on a GNU system (where grep is usually significantly faster than GNU awk):

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -newermt '2016-09-20 10:06:42' -print0 |
  xargs -r0P 4 -n 100 sh -c '
    zcat "$@" | 
      grep --line-buffered -E \
        "^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)"
  ' sh | head)

Note that in the parallel approach, you may get the output of the grep commands inter-mingled (though with line-buffering and provided lines are less than a few kilobytes large, line boundaries should be preserved).

1
  • 1
    Maybe you can also play with xargs -n 100 to let the first zcat start earlier. Generally it's worth to note that if awk is not running on 100% CPU then we don't need to optimize awk at all but the command earlier in the pipe and so on.
    – rudimeier
    Commented Sep 22, 2016 at 10:47
0

@Stéphane Chazelas's answer provides a great many details on how you can optimize the command pipeline

find . -name "muc*_*_20160920_*.unl*" | xargs zcat |
    awk -F "|" '{if($14=="20160920100643" && $22=="567094398953") print $0}'| head

I'm going to provide another way to approach the problem where you actually measure where you're spending the most time. Once you find where the time is spent, you can determine what to do about it. If you want to improve your 10-minute run time, optimizing a step that takes 2 seconds is almost useless.

When I look at the command pipeline, three things draw my attention:

  1. find . - What is the directory structure like? How many files per directory? Is the directory local to the system the command is being run on? A remote filesystem is going to be a lot slower.
  2. -name "muc*_*_20160920_*.unl*" - How close are all the file names in the directory structure? Are they all "close" to the name and difficult/CPU intensive to match? Because every file in the directory tree has to have its name read from disk and compared to the pattern.
  3. xargs zcat - The xargs doesn't seem to me that it will be too much of a performance problem, especially compared to the find issues above and the zcat itself. Even if it's 10,000 or even 10,000,000 file names, the time used in passing and parsing just the names is almost certainly negligible compared to the time spent finding the names and then opening and decompressing all the files themselves. How large are the files? Because you're decompressing the entirety of every file that matches your find's filename pattern.

How can you determine what's the major performance problem? Measure the performance of each command in the pipeline. (See https://stackoverflow.com/questions/13294554/how-to-use-gnu-time-with-pipeline for details on timing an entire pipeline.) You can run the following commands and see how much time each step contributes to the processing time for the entire pipeline:

/usr/bin/time find . - This tells you how long it takes to run through your directory tree. If this is slow, you need a better storage system. Flush your filesystem cache[s] before timing this to get a worst-case measurement, then run the timed find again and see how much caching impacts performance. And if the directory isn't local, try running the command on the actual system that the files are on.

/usr/bin/time find . -name "muc*_*_20160920_*.unl*" - This will tell you how long it takes to pattern-match the file names. Again, flush filesystem cache[s] and run it twice.

/usr/bin/time bash -c "find . -name 'muc*_*_20160920_*.unl*' | xargs zcat > /dev/null" - This is the one I suspect is the major component of your pipeline's long running time. If this is the problem, parallelizing the zcat commands per Stéphane Chazelas answer may be the best answer.

Continue adding steps from the original command pipeline to the one being tested until you find where you're spending most of your time. Again, I suspect it's the zcat step. If so, perhaps the zcat parallelization that @Stéphane Chazelas posted will help.

Parallelizing zcat may not help - it may even hurt performance and slow processing down. With only one zcat running at a time, IO may be in a nice streaming pattern that minimizes disk seeks. With multiple zcat processes running at once, the IO operations may contend and actually slow down processing as disk heads need to seek and any read-ahead done becomes less effective.

If the zcat step is your major performance bottleneck, and running multiple zcat processes at one time doesn't help or actually slows you down, your pipeline is IO-bound and you need to address the problem by using faster storage.

And again - if the directory isn't local the machine your running the command pipeline on, try running it on the machine that the file system actually is on.

-1

As noted in comments zgrep is better choice for such kind of tasks with globstar option which allows to use ** as all path inside the directory except hidden

shopt -s globstar
zgrep -m 10 '^\([^|]*|\)\{13\}20160920100643|\([^|]*|\)\{7\}567094398953' ./**muc*_*_20160920_*.unl*
shopt -u globstar
11
  • Recursive globbing is primarily a zsh feature, shopt -s gobstar is bash-only syntax. **muc*_*_20160920_*.unl* is fish only syntax. With zsh or bash -O globstar or ksh93 -o globstar, you need **/muc*_*_20160920_*.unl*. In any case, using shell recursive globbing is not going to be more efficient than using find. zgrep is inneficient as it runs one gzip and one grep per file. Commented Sep 22, 2016 at 10:17
  • Does not work if there are too many files (xargs --show-limits).
    – rudimeier
    Commented Sep 22, 2016 at 10:17
  • Hi Costas, this worked with just a little twick, got the output in 4 mins. Don't really understand your command but it worked for me. Thanks
    – yemmy
    Commented Sep 22, 2016 at 10:54
  • 1
    zgrep '([^|]*|\+)\{13\}20160920100643|\+([^|]*|\+)\{7\}567094398953' *muc*_20160920.unl | head
    – yemmy
    Commented Sep 22, 2016 at 10:54
  • Will appreciate a little explanation, most especially the {13\} and {\7}
    – yemmy
    Commented Sep 22, 2016 at 11:09
-1

As pointed out, it is not possible to give the correct answer without some extra details.

locate -0 -b -r '^muc.*_.*_20160920_.*.unl.*gz' | 
   xargs -0  zcat |
   awk -F "|" '$14=="20160920100643" && $22=="567094398953"'| head
  • 1: locate (if available) is much faster than ** or find; The regular expression used must be tuned...

  • 2 and 3: the PO's filter

As @rudimeier, wisely pointed out, there are issues regarding the availability and update state of locate. (eg in most linux machines locate is update daily; this way it will fail to find files created today)

Nevertheless if locate is available this will produce a very impressive speedup.

It would be interesting if the PO could provide the time ... of the various solutions

7
  • I don't think that locate is appropriate here. If there are really millions of files which makes find slow, then the locatedb is probably also big and slow. And locatedb updates (cron) may slow down the machine for hours or days.
    – rudimeier
    Commented Sep 22, 2016 at 10:05
  • @rudimeier, if availlable (which happens by default in most of linux machines) locate if very fast!. It uses database indexing techniques. In my machine (2 terabytes of files) a locate like the one presented in my answer, takes less than 1s; a similar find or "**" takes more than 1 hour just to search in my home/jj folder.
    – JJoao
    Commented Sep 22, 2016 at 11:58
  • Why is this downvoted?
    – JJoao
    Commented Sep 22, 2016 at 12:01
  • My concern about locate is more because of reliability rather than speed. He is grepping for yesterday's dates! How to make sure that locatedb is up-to-date? BTW your zgrep is slower than the original zcat (It's just a huge shell script which runs one zcat per_file). Moreover "-m10" is simply wrong.
    – rudimeier
    Commented Sep 22, 2016 at 12:24
  • @rudimeier, thank you for the comments. (1) you are right about "-m 10" - it may remove some relevant results (updated)
    – JJoao
    Commented Sep 22, 2016 at 14:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .