I wrote a parallel gzip decoder, rapidgzip (previously "pragzip"), which can speed up this use case. Here is a performance comparison for counting newlines. I also added igzip
, part of the isal package, to the comparison because it is the fastest single-core gzip decompressor I know of. And I included pigz because it is often mentioned for parallel gzip handling but that is only for compression while it doesn't actually use all cores for decompression.
Decoder |
Lines |
Runtime / s |
Bandwidth / (MB/s) |
rapidgzip --count-lines |
55778796 |
0.879 |
4886 |
rapidgzip |
55778796 |
1.309 |
3281 |
igzip |
55778796 |
9.113 |
471 |
pigz |
55778796 |
13.091 |
328 |
gzip |
55778796 |
22.132 |
194 |
zgrep |
55778797 |
23.061 |
186 |
Tested on a Ryzen 3900X with 64 GB of RAM. Of course, you will either need a fast enough SSD or use an in-memory file system like /dev/shm
, or depending on the distribution /tmp
, to reach these speeds.
As others have mentioned, further speedups can be reached by specializing the decoder and letting it do the counting internally instead of copying and piping the decoded data to another tool. This is what rapidgzip --count-lines
does.
Furthermore, rapidgzip still contains many things that can be further optimized. Note that at this point you will very likely run into I/O problems first because the disk also has to keep up.
Lastly, beware that the result for zgrep as mentioned in the other answer (zgrep -c '$' 4GiB-base64.gz
) is off by one because wc -l
counts the number of \n
while zgrep -c '$'
effectively counts every line including the last one even if it does not end with \n
!
This is the script I used to benchmark:
sudo apt install pigz isal
python3 -m pip install --user --upgrade rapidgzip
# Create a compressible random file with many newlines
base64 /dev/urandom | head -c $(( 4 * 1024 * 1024 * 1024 )) > 4GiB-base64
gzip -c 4GiB-base64
fileSize=$( stat -L --format=%s 4GiB-base64 )
printf '\n| %7s | %8s | %10s | %18s |\n' Decoder Lines 'Runtime / s' 'Bandwidth / (MB/s)'
printf -- '|---------|----------|-------------|--------------------|\n'
for tool in rapidgzip igzip pigz gzip; do
lineCount=$( $tool -d -c "4GiB-base64.gz" | wc -l )
runtime=$( ( time $tool -d -c "4GiB-base64.gz" | wc -l ) 2>&1 | sed -n -E 's|real[ \t]*0m||p' | sed 's|[ \ts]||' )
bandwidth=$( python3 -c "print( int( round( $fileSize / 1e6 / $runtime ) ) )" )
printf '| %7s | %8s | %11s | %18s |\n' "$tool" "$lineCount" "$runtime" "$bandwidth"
done
lineCount=$( zgrep -c '$' "4GiB-base64.gz" )
runtime=$( ( time zgrep -c '$' "4GiB-base64.gz" ) 2>&1 | sed -n -E 's|real[ \t]*0m||p' | sed 's|[ \ts]||' )
bandwidth=$( python3 -c "print( int( round( $fileSize / 1e6 / $runtime ) ) )" )
printf '| %7s | %8s | %11s | %18s |\n' "zgrep" "$lineCount" "$runtime" "$bandwidth"
lineCount=$( rapidgzip --count-lines "4GiB-base64.gz" )
runtime=$( ( time rapidgzip --count-lines "4GiB-base64.gz" ) 2>&1 | sed -n -E 's|real[ \t]*0m||p' | sed 's|[ \ts]||' )
bandwidth=$( python3 -c "print( int( round( $fileSize / 1e6 / $runtime ) ) )" )
printf '| %7s | %8s | %11s | %18s |\n' "rapidgzip --count-lines" "$lineCount" "$runtime" "$bandwidth"
Rapidgzip can also be used as a library from Python. Line counting could be done like this:
python3 -m pip install --user rapidgzip
import rapidgzip
result = 0
with rapidgzip.open("silesia.tar.gz") as file:
while chunk := file.read( 1024*1024 ):
result += chunk.count(b'\n')
print("Number of newlines:", result)
Note that performance is significantly impacted by the chunk size used for the read call and even Python's count function might be a bottleneck. I have not benchmarked this implementation.