SWIGでC/C++をラップしPythonの処理を高速化する。【概要編】

# SWIGとは？
SWIGは、C/C++で書かれたプログラムをラップし、多言語で使えるようにするためのツールです。
サポートされている言語には、Javascript、Perl、PHP、Python、Tcl、Rubyなどのスクリプト言語や、C#、D、Go、Java、Lua、OCaml、Octave、Scilab、Rなどの非スクリプト言語があります。

# 速度比較
C/C++で書かれたコードが早いのはよく知られていますが、実際にどれくらいの速度差があるのか、いくつかの条件で比較します。

<実行環境>
OS: Ubuntu18.04
CPU: Intel Corei7-7700k
メモリ: 24GB

<コンパイル環境>
-O3オプションあり
並列化なし

## Hello World
"hello world!"と1000回表示する関数の実行速度

```
python_time:2.356291e-03[sec]
swig_time__:1.398325e-03[sec]
```
2倍程度の差がありますが、一般的に言われているほどの差があるようには見えません。
厳密にはわかりませんが、文字列のコンソール表示速度のオーバーヘッドがあるからだと思われます。


<details>
<summary>コード</summary>
<div>

```python:python
def hello_world():
    for i in range(1000):
        print("hello world!")

def test1():
    # python
    start = time.time()
    hello_world()
    python_time = time.time() - start
    # swig
    start = time.time()
    helloWorld()
    swig_time = time.time() - start
    print("python_time:{:e}".format(python_time) + "[sec]")
    print("swig_time__:{:e}".format(swig_time) + "[sec]")
```
```c:c
void helloWorld()
{
    for (int i = 0; i < 1000; i++)
    {
        printf("hello world!\n");
    }
}
```
</div>
</details>

## 数字のカウントアップ
1000までをカウントアップして返す関数の実行速度

```
python_time:6.842613e-05[sec]
swig_time__:5.483627e-06[sec]
```
今度は10倍以上の差が出ました。数字をカウントアップするだけでここまで差が出ると、pythonは遅いといわれる理由が垣間見えてきます。

<details>
<summary>コード</summary>
<div>

```python:python
def count_up():
    res = 0
    for i in range(1000):
        res += 1
    return res

def test2():
    # python
    start = time.time()
    res = count_up()
    print(res)
    python_time = time.time() - start
    # swig
    start = time.time()
    res = countUp()
    print(res)
    swig_time = time.time() - start
    print("python_time:{:e}".format(python_time) + "[sec]")
    print("swig_time__:{:e}".format(swig_time) + "[sec]")
```
```c:c
int countUp()
{
    int res = 0;
    for (int i = 0; i < 1000; i++)
    {
        res += 1;
    }
    return res;
}
```
</div>
</details>

## 画像の変換
グレースケール画像からRGB画像へ変換する関数の実行速度
※Python側ではOpenCVモジュールを使用

```
python_time:1.032352e-04[sec]
swig_time__:1.156330e-04[sec]
```
こちらはほとんど変わらない結果になりました。OpenCVはそもそもC/C++で書かれているので、当然といえば当然の結果ですね。
しかし、このSWIGの実行速度には、np.zeros()で出力先のメモリを確保しておく処理が含まれています。OpenCVのpythonパッケージを利用している場合には不可能ですが、SWIGで書いている場合には出力先メモリを再利用することが可能です。
メモリ空間の確保を外だしした場合の速度は以下のようになります。

```
python_time:1.101494e-04[sec]
swig_time__:7.319450e-05[sec]
```
この条件だとSWIGが30%以上高速化しています。複数回実行する場合はSWIGでのベタ書きにも優位性がありそうです。

<details>
<summary>コード</summary>
<div>

```python:python
def test3():
    img_size = (256, 256)
    org_img = np.random.randint(0, 256, (img_size), dtype=np.uint8)
    # python
    start = time.time()
    res_py = cv2.cvtColor(org_img, cv2.COLOR_GRAY2RGB)
    python_time = time.time() - start
    # swig
    # res_swig = np.zeros((*img_size, 3), dtype=np.uint8)
    start = time.time()
    res_swig = np.zeros((*img_size, 3), dtype=np.uint8)
    imgGray2RGB(org_img, res_swig)
    swig_time = time.time() - start
    print("array_equal: {}".format(np.array_equal(res_py, res_swig)))
    print("python_time:{:e}".format(python_time) + "[sec]")
    print("swig_time__:{:e}".format(swig_time) + "[sec]")
```
```c:c
void imgGray2RGB(unsigned char *inArr, int inDim1, int inDim2,
                 unsigned char *inplaceArr, int inplaceDim1, int inplaceDim2, int inplaceDim3)
{
    int height = inplaceDim1;
    int width = inplaceDim2;
    int channel = inplaceDim3;
    int h, w;
    int in_point, out_point;

    for (h = 0; h < height; h++)
    {
        for (w = 0; w < width; w++)
        {
            in_point = h * width + w;
            out_point = channel * (h * width + w);
            inplaceArr[out_point] = inArr[in_point];
            inplaceArr[out_point + 1] = inArr[in_point];
            inplaceArr[out_point + 2] = inArr[in_point];
        }
    }
}
```
</div>
</details>

### (オプション) 画像の正規化
グレースケールからRGBへの変換に加え、画像の正規化を行った実行速度
※Python側ではOpenCVモジュールを使用

```
python_time:1.460791e-03[sec]
swig_time__:3.521442e-04[sec]
```
OpenCVもC/C++で実装しているはずですが、SWIGがOpenCVより4倍の速度が出ています。
これは、一度のラスタ走査ですべての処理を終えており、処理の量が大きく削減されているためです。OpenCVのPythonパッケージは当然ながら関数単位でわかれているため、こういった処理の削減で高速化を狙う場合は、C/C++での処理が必須になります。

<details>
<summary>コード</summary>
<div>

```python:python
def test4():
    img_size = (256, 256)
    mean = [0.485, 0.456, 0.406]
    std = [0.229, 0.224, 0.225]
    mean_np = np.array(mean, dtype=np.float32)
    std_np = np.array(std, dtype=np.float32)
    org_img = np.random.randint(0, 256, (img_size), dtype=np.uint8)
    # python
    start = time.time()
    res_py = cv2.cvtColor(org_img, cv2.COLOR_GRAY2RGB).astype(np.float32)
    res_py = ((res_py / 255) - mean_np) / std_np
    python_time = time.time() - start
    # swig
    start = time.time()
    res_swig = np.zeros((*img_size, 3), dtype=np.float32)
    imgNormalize(org_img, res_swig, *mean, *std)
    swig_time = time.time() - start
    print("array_equal: {}".format(np.array_equal(res_py, res_swig)))
    print("python_time:{:e}".format(python_time) + "[sec]")
    print("swig_time__:{:e}".format(swig_time) + "[sec]")
```
```c:c
void imgNormalize(unsigned char *inArr, int inDim1, int inDim2,
                  float *inplaceArr, int inplaceDim1, int inplaceDim2, int inplaceDim3,
                  float meanR, float meanG, float meanB,
                  float stdR, float stdG, float stdB)
{
    int height = inplaceDim1;
    int width = inplaceDim2;
    int channel = inplaceDim3;
    int h, w;
    int val;
    int inPoint, outPoint;

    for (h = 0; h < height; h++)
    {
        for (w = 0; w < width; w++)
        {
            inPoint = h * width + w;
            outPoint = channel * (w + width * h);
            val = inArr[inPoint];
            inplaceArr[outPoint] = ((float)val / 255 - meanR) / stdR;
            inplaceArr[outPoint + 1] = ((float)val / 255 - meanG) / stdG;
            inplaceArr[outPoint + 2] = ((float)val / 255 - meanB) / stdB;
        }
    }
}
```
</div>
</details>

# 次回
次は実装編になります。実装編では基本的なSWIGの使い方から、Numpyを直接引数へ渡してC/C++側からポインタ参照する方法など、ちょっとした応用を紹介予定です。