alpineでC言語依存モジュールを pip install した時の時間を計測してみた

# Let's 計測
[alpineでC言語依存モジュールを pip install すると激重になる話](https://qiita.com/naka345/items/d1e05ac9649ff2292ef0) のおまけ

素直にpip installした場合に、どれくらい時間がかかるか試してみた。
手元にあった機械学習用の`requirements.txt`で実行してみると以下のようなログがでてきたので

```
Building wheels for collected packages: backcall, h5py, kiwisolver, matplotlib, numpy, pandas, pathspec, Pillow, PyYAML, pyzmq, regex, retrying, scikit-learn, scipy, tornado, typed-ast
```

開発時にも使用頻度高そうなモジュールだけピックアップして計測することにした。

* 実行してみるリスト
 * numpy
 * pandas
 * Pillow
 * scipy
 * scikit-learn
 * h5py
 *  matplotlib
 * regex

## 実験環境
手元で実行できる環境が二種類あったので両方とも試してみる。

* case1: MacBookAir 2015 13inch intel-corei5 8GB DDR3 ・ docker desktop
* case2: AMD Ryzen9 3900X, M.2 ASM2NE6500GTTD, 8x2GB DDR4 ・ ubuntu18.04(WSL2 integration)

## 計測方法
timeコマンドを頭につける事で実行時間を計測する。
どちらのケースも同一のdockerfileで実行しており、`RUN`命令文中に`time pip install hoge`を埋め込む形を取っている。

```dockerfile

FROM python:3.8-alpine3.11
RUN apk update \
  && apk add --virtual .build --no-cache openblas-dev lapack-dev freetype-dev \
  gfortran libxml2 g++ gcc zip unzip cmake make \
  libpng-dev openssl-dev musl libffi-dev python3-dev libxslt-dev \
  libxml2-dev jpeg-dev \
  && apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing hdf5-dev

RUN pip install --upgrade --no-cache-dir pip setuptools wheel && \
  time pip install --no-cache-dir Cython && \
...
```
こんな感じ。
`docker build`中に出力されるログをまとめたのが次の表となる。

## 結果
※ Cythonはalpineでもコンパイル不要のモジュールであり、`Cython-0.29.16-py2.py3-none-any.whl`のような`.whl`形式で降ってきます。非依存モジュールとの比較としてここに載せています。[^1]

[^1]: [ここ](https://stackoverflow.com/a/46915474)に書いてあるように、`none-any`は非OS・アーキテクチャ依存のモジュールでほとんどの場合pureなpythonで作成されています。[公式PEP425](https://www.python.org/dev/peps/pep-0425/)も参照。

<table>
    <tr>
      <th rowspan="2">module / time</th>
      <th colspan="3" text-align="center">case1</th>
      <th colspan="3" text-align="center">case2</th>
    </tr>
    <tr>
      <td>real</td>
      <td>user</td>
      <td>sys</td>
      <td>real</td>
      <td>user</td>
      <td>sys</td>
    </tr>
    <tr>
      <td>Cython</td>
      <td>3.07s</td>
      <td>2.33s</td>
      <td>0.42s</td>
      <td>1.52s</td>
      <td>0.99s</td>
      <td>0.07s</td>
    </tr>
    <tr>
      <td>numpy</td>
      <td>5m 28.27s</td>
      <td>7m 6.90s</td>
      <td>22.11s</td>
      <td>2m 11.15s</td>
      <td>3m 23.69s</td>
      <td>6.05s</td>
    </tr>
    <tr>
      <td>pandas</td>
      <td>31m 46.58s</td>
      <td>30m 36.25s</td>
      <td>0m 53.83s</td>
      <td>14m 8.24s</td>
      <td>13m 53.10s</td>
      <td>15.88s</td>
    </tr>
    <tr>
      <td>Pillow</td>
      <td>50.81s</td>
      <td>44.09s</td>
      <td>5.99s</td>
      <td>24.79s</td>
      <td>19.88s</td>
      <td>1.69s</td>
    </tr>
    <tr>
      <td>scipy</td>
      <td>30m 45.99s</td>
      <td>36m 29.33s</td>
      <td>1m 45.89s</td>
      <td>12m 52.81s</td>
      <td>17m 54.87s</td>
      <td>42.58s</td>
    </tr>
    <tr>
      <td>scikit-learn</td>
      <td>14m 38.63s</td>
      <td>14m 3.37s</td>
      <td>28.98s</td>
      <td>6m 33.10s</td>
      <td>6m 24.84s</td>
      <td>10.11s</td>
    </tr>
    <tr>
      <td>h5py</td>
      <td>3m 45.58s</td>
      <td>3m 34.79s</td>
      <td>9.30s</td>
      <td>1m 45.87s</td>
      <td>1m 42.42s</td>
      <td>4.18s</td>
    </tr>
    <tr>
      <td>matplotlib</td>
      <td>2m 51.50s</td>
      <td>2m 35.53s</td>
      <td>13.59s</td>
      <td>1m 21.77s</td>
      <td>1m 13.70s</td>
      <td>6.75s</td>
    </tr>
    <tr>
      <td>regex</td>
      <td>30.52s</td>
      <td>28.84s</td>
      <td>1.24s</td>
      <td>13.75s</td>
      <td>13.06s</td>
      <td>0.34s</td>
    </tr>
</table>

おっっっっそい
ものっっっっっそい、おっっっっっっそい

case1だとnumpyだけでも5分かかるし、pandas・scipyに至っては30分を超えている。scikit-learnに至ってはnumpy・scipyに依存しているので実際のところ一時間は覚悟しないといけない。
挙げた全部合わせると一時間半超だ。
これがどういう事になるか。一日8時間作業すると仮定すると

**一日たったの5回程度しかデプロイする事ができない**ことになる

気軽に開発環境に最新ブランチを充てたり、軽微なバグ修正の確認すら困難になる。作業時間も含めると更に減る。

一応、メモリが倍になると処理速度も倍になるので時間の短縮はできなくもない。
awsを使ったりしているのならメモリ量が多いインスタンスタイプを選択することで時短は可能だ。
しかし、性能的にcase1の二倍以上のcase2だと半分以下の時間で済むとはいえ、全部で40分ほどかかっているのは事実。
一日あたり最高で12回ほどのデプロイしかできない。

----

ちなみに、表の上から順番に`pip install`していったので一部モジュールの実行時間には依存モジュールのインストール実行時間も含まれる。
kiwisolverも`tar.gz`で降ってきているようなのでmatplotlib単体はもう少し早いはず。

```
  Successfully built pandas
  Installing collected packages: six, python-dateutil, pytz, pandas
  Successfully installed pandas-0.25.3 python-dateutil-2.8.1 pytz-2019.3 six-1.14.0
------
  Successfully built scikit-learn
  Installing collected packages: joblib, scikit-learn
  Successfully installed joblib-0.14.1 scikit-learn-0.22.2.post1
-----
  Successfully built matplotlib kiwisolver
  Installing collected packages: cycler, kiwisolver, pyparsing, matplotlib
  Successfully installed cycler-0.10.0 kiwisolver-1.2.0 matplotlib-3.2.1 
```

# まとめ
軽いものでも1個あたり1~2分かかるレベルなので、なるべくC言語依存モジュールはビルドしないで済む運用を考える必要がある。~~condaを使う、は今無しで~~

## 余談pip
`pip install`時はモジュールのwheel化が完了した直後に、再度whlを展開し直して必要な生成物をまるっと`site-package`直下に移動する。pythonで書かれたコードからビルドされた共有ライブラリ郡`*.so`も含めて全てだ。
alpineのように`python3.8`のバイナリが`/usr/local/bin`直下にいるとすると`/usr/local/lib/python3.8/site-packages/`に移動する。

再展開は手間のように見えるがこれはPEPに則った安全なモジュールの導入法である。[PEP491](https://www.python.org/dev/peps/pep-0491/#details)にもそう書かれている[^2]

[^2]: 余談中の余談だがanacondaは独自のモジュールインストール方法を採っているのでPEPに即していない。混ぜるな危険を引き起こしている一端。

---
試しに、ビルド済みのwhlファイルを`unzip`して`.so`ファイルを検索してみる。

```
./numpy/linalg/lapack_lite.cpython-38-x86_64-linux-gnu.so
./numpy/linalg/_umath_linalg.cpython-38-x86_64-linux-gnu.so
./numpy/core/_operand_flag_tests.cpython-38-x86_64-linux-gnu.so
...
```

それぞれのwhlファイルを`pip install`で導入後にライブラリパスに対して`.so`ファイルを検索してみると、モジュール毎にビルド済みファイルが存在しているのを確認できる。

```
/usr/local/lib/python3.8/site-packages/numpy/linalg/lapack_lite.cpython-38-x86_64-linux-gnu.so
/usr/local/lib/python3.8/site-packages/numpy/linalg/_umath_linalg.cpython-38-x86_64-linux-gnu.so
/usr/local/lib/python3.8/site-packages/numpy/core/_operand_flag_tests.cpython-38-x86_64-linux-gnu.so
...
/usr/local/lib/python3.8/site-packages/pandas/io/sas/_sas.cpython-38-x86_64-linux-gnu.so
/usr/local/lib/python3.8/site-packages/pandas/_libs/hashtable.cpython-38-x86_64-linux-gnu.so
...
```

これでようやくpythonでimportできる訳だが、
裏を返せばpythonがモジュールを検索するパスに生成物さえ居れば使用できる。
なので[元記事の回避法](https://qiita.com/naka345/items/d1e05ac9649ff2292ef0#%E5%9B%9E%E9%81%BF%E7%AD%96)では、python関連以外は後に入れること前提で`/usr/local/`直下をdockerのマルチステージビルドを使ってえいやでコピペしている。

# 参考文献
- [Wheel files : What is the meaning of “none-any” in protobuf-3.4.0-py2.py3-none-any.whl](https://stackoverflow.com/a/46915474)
- [PEP 425 -- Compatibility Tags for Built Distributions](https://www.python.org/dev/peps/pep-0425/)
- [PEP 491 -- The Wheel Binary Package Format 1.9](https://www.python.org/dev/peps/pep-0491/)
- [wheelのありがたさとAnacondaへの要望](https://ymotongpoo.hatenablog.com/entry/2017/02/02/182647)
  - wheel・PEP関連ですごく参考にしました。
    - ちなみに現在だと`conda skeleton`でPyPiのwheelを導入できる
      - ただし、conda独自にビルドし直して導入している模様。個人的には ｿｳｼﾞｬﾅｲ感
      - [anacondaにsentencepieceをwheelファイル経由でインストールする](https://qiita.com/toriwasa/items/5e1b4fef73855c2e9a29)