コマンド3行だけでZIPファイルを3倍高速に展開する！

# はじめに

みずほリサーチ＆テクロノジーズの @fujine です。
本記事では、**LinuxでZIPファイルを高速に展開するテクニック**を解説します。ご紹介するケースでは、**4vCPUマシンで約3倍速くなりました**。CPUコア数が多ければもっと速くなります！

お急ぎの方は、こちらをコピペして下さい。
`ZIPファイルのパス`を書き換えて実行するだけで動きます。
```
sudo apt install -y parallel

zipile=ZIPファイルのパス
zipinfo -1 $zipfile | parallel --bar "unzip -q $zipfile {}"
```

![bar.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969456/9118e7df-b52a-32d0-af43-256ae0c17e7c.gif)

# モチベーション
Kaggle等のデータ分析コンペでは、データセットをZIP形式でダウンロードすることが多いです。数GBのZIPファイルなら展開もすぐに終わりますが、**数十〜数百GBの巨大ZIPになると、展開するだけで数十分（下手したら数時間！）かかる** なんてこともあります。

また、コンペでよく使われるGoogle Colabやクラウドコンピューティングの多くは従量課金制です。**展開に時間が掛かるほど費用も積み上がってしまう**ため、「短時間で展開できればお財布にも優しい」というメリットがあります💰

そこで、ZIPファイルをなるべく高速に展開する方法を模索しました。

# 検証環境
Debian環境で検証します。CPUコア数は4です。

```
$ cat /etc/debian_version 
11.8

$ lscpu | grep On-line
On-line CPU(s) list:             0-3
```

# 使用するデータセット
Kaggleの[Petals to the Metal - Flower Classification on TPU](https://www.kaggle.com/competitions/tpu-getting-started)コンペのデータセットを使用します。

[Kaggle API](https://github.com/Kaggle/kaggle-api)を使用している方は、データセットを以下コマンドでダウンロードできます。

```
$ kaggle competitions download tpu-getting-started
```

ZIPファイルのサイズは4.8GB、圧縮されているファイル数は193件です。

```
$ zipfile=tpu-getting-started.zip

$ du -h $zipfile
4.8G    tpu-getting-started.zip

$ zipinfo -1 $zipfile | wc -l
193
```

# 通常の方法で展開
まずは通常のunzipコマンドで展開します。所要時間は約53秒でした。

```
$ time unzip -q $zipfile

real    0m53.407s
user    0m37.133s
sys     0m15.895s
```

# より高速に展開
ZIP内の各ファイルをマルチコアで並列展開することで、高速化を図ります。並列処理には[GNU parallel](https://www.gnu.org/software/parallel/)パッケージを使用します。
```
$ sudo apt install -y parallel
```
以下コマンドで並列展開します。やっていることはシンプルで、
- `zipinfo -1 $zipfile`はZIP内のファイル名を一覧出力し、パイプに渡す
- `parallel`はファイル名を引数として受け取り、ジョブの`{}`に代入する
- `unzip -q $zipfile {ファイル名}`ジョブが並列で実行される

という流れです。

```
$ time zipinfo -1 $zipfile | parallel "unzip -q $zipfile {}"

real    0m18.243s
user    0m53.139s
sys     0m13.357s
```
所要時間は約18秒となり、先ほどのunzipよりも**3倍弱ほど高速化に成功**しました。仕組み上、CPUコア数が多いマシンほど更に高速に展開できるはずです。

# 有効なシーン
**ZIPファイルに多数ファイルが圧縮されており、かつ、複数のCPUコアを搭載したマシンであるほど効果的**です。

逆に、CPUコアが1つのみだったり、ZIPファイルに少数のファイルしか圧縮されていない場合、効果は薄いです。

# parallelの便利なオプション
これだけでも十分便利ですが、`parallel`には他にも様々なオプションが用意されています。
以下、個人的に便利だと思ったオプションをいくつかご紹介します。

## 進捗を表示
`--progress`で、ジョブの実行数やCPU使用率などが出力されます。
```
$ zipinfo -1 $zipfile | parallel --progress "unzip -q $zipfile {}"
```

![progress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969456/3f4a19b2-831c-a18d-8293-28b8532752fa.gif)

`--bar`にすれば、プログレスバーとともに進捗率や残り時間が表示されるため、視覚的にも分かりやすいです。
```
$ zipinfo -1 $zipfile | parallel --bar "unzip -q $zipfile {}"
```
![bar.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969456/9118e7df-b52a-32d0-af43-256ae0c17e7c.gif)

## バックグラウンドで実行
`--bg`でジョブがバックグランド実行されます。並列処理を裏で実行しつつ、すぐ別の作業に取り掛かりたい時に便利です。
```
$ zipinfo -1 $zipfile | parallel --bg "unzip -q $zipfile {}"
$ 
```

## 実行前にコマンドを確認
`--dry-run`で、実行されるコマンドが出力されます（実際には実行されません）。コマンドが正しいかを実行前に確認するのに重宝します。
```
$ zipinfo -1 $zipfile | parallel --dry-run "unzip -q $zipfile {}" | head -n 5
```
```
unzip -q tpu-getting-started.zip sample_submission.csv
unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/00-192x192-462.tfrec
unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/01-192x192-462.tfrec
unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/02-192x192-462.tfrec
unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/03-192x192-462.tfrec
```

## ジョブの最大並列数を指定
`--jobs`で最大並列数を指定します。デフォルトは全てのCPUコアが利用されるため、最大並列数を下げることで、他の実行中プロセスへの影響を低減できます。

指定値は整数（コア数）だけでなく、`50%`のように指定することも可能です。

```
# 最大で3並列
$ zipinfo -1 $zipfile | parallel --jobs 3 "unzip -q $zipfile {}"

# 最大で50%
$ zipinfo -1 $zipfile | parallel --jobs 50% "unzip -q $zipfile {}"
```

## 引数の1件目をスキップ
`--skip-first-line`で、引数の1件目のみをスキップさせます。

どこで役立つかというと、例えばCSVファイルの各行をパイプで渡す際、1行目のヘッダーだけスキップし、2行目以降のデータだけを並列処理させる、といったことが簡単にできます。

## メモリをスワップさせない
`--noswap`を指定すれば、メモリのスワップが発生している時に新しいジョブを実行しないように制御できます。

並列展開するとCPUコア数に比例してメモリ消費量も増えてしまうため、このオプションだけで**スワップ回避まで面倒を見てくれる**のは非常に有難いです。

## 実行結果を出力
`--joblog ログファイル名`で、実行結果がログ出力されます。

各ジョブの実行時間(`JobRuntime`)や終了ステータス(`Exitval`)などが出力されるため、失敗したジョブや遅いジョブがあればすぐに特定できます。
```
$ zipinfo -1 $zipfile | parallel --joblog report.txt "unzip -q $zipfile {}"
$ head report.txt 
```
```
Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1697803718.751       0.012      0       0       0       0       unzip -q tpu-getting-started.zip sample_submission.csv
3       :       1697803718.759       0.161      0       0       0       0       unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/01-192x192-462.tfrec
2       :       1697803718.755       0.171      0       0       0       0       unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/00-192x192-462.tfrec
4       :       1697803718.763       0.175      0       0       0       0       unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/02-192x192-462.tfrec
5       :       1697803718.769       0.184      0       0       0       0       unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/03-192x192-462.tfrec
6       :       1697803718.925       0.184      0       0       0       0       unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/04-192x192-462.tfrec
7       :       1697803718.930       0.190      0       0       0       0       unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/05-192x192-462.tfrec
8       :       1697803718.944       0.190      0       0       0       0       unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/06-192x192-462.tfrec
9       :       1697803718.963       0.187      0       0       0       0       unzip -q tpu-getting-started.zip tfrecords-jpeg-192x192/test/07-192x192-462.tfrec
```

# 終わりに
本記事では、GNUの`parallel`パッケージを活用したZIPファイルの並列展開をご紹介しました。
`parallel`はZIP展開のみならず、並列処理可能な様々なユースケースに応用できそうです。

# 参考リンク
- [GNU Parallel's documentation](https://www.gnu.org/software/parallel/sphinx.html)
- [コマンドを並列に実行するGNU parallelがとても便利](https://bicycle1885.hatenablog.com/entry/2014/08/10/143612)