TensorFlowを活用してC++実装とPython実装で推論処理の性能比較をしてみた

TensorFlowはC++実装であるため、C++とPythonの2つの言語で実装できます。今回はC++とPythonの双方で推論処理を実装し、性能を比較評価していきます。NVIDIA A100のベンチマークも兼ねています。当然C++の方が高性能を期待できるので、Pythonがどれくらい迫れるかが楽しみですね。

今回評価に使ったソースコードは [shohata/tensorflow_perf](https://github.com/shohata/tensorflow_perf) に置いてあります。

:::note
この記事は[こちら](https://manacra.com/tensorflow-benchmark/)でも紹介しています。
:::

## 評価項目

今回の評価では大量の画像を推論し、そのトータルの時間を計測します。評価する項目は次の通りです。

- 総推論数 (Total Predictions)
- 経過時間 (Duration Time)
- スループット (Throughput)
- レイテンシ (Latency)

総推論数は大体同じならば良いものとします。スループットは総推論数を経過時間で割った値です。レイテンシは経過時間を総合推論数で割った値です。

## 評価条件

MLの評価条件は次の通りです。

| 項目 | 値 |
| --- | --- |
| データセット | IMAGENET2012 |
| MLモデル | ResNet50 |
| バッチ数 | 32 |
| フレームワーク | TensorFlow v2.14.0 |
| CUDA | 11.8 |
| cuDNN | 8.7 |

サーバスペックは次の通りです。

| 項目 | 値 |
| --- | --- |
| OS | Ubuntu 22.04.4 LTS |
| GPU | NVIDIA A100 80GB |
| CPU | Intel Xeon Processor (Icelake) |
| コア数 | 16 |
| スレッド数 | 16 |
| メモリ | 64GB |

評価は次の3パターンで行います。

1. ジョブシステム（ワーカー数=4）
1. ジョブシステム（ワーカー数=1）
1. forループ

ジョブシステムを使ってマルチスレッドで推論するのが最も高速であることを期待できるパターン。ジョブシステムを使ってシングルスレッドで推論するのが１バッチにかかる時間、つまりレイテンシを測定することが期待できるパターン。最後にforループを使ってC++の高速性が期待できるパターン。これら3つのパターンを考えました。

## C++実装の結果

C++実装のソースコードと実行結果です。

- [src/benchmark.cpp](https://github.com/shohata/tensorflow_perf/blob/ae4fbcfb6790c2cd8dcb938ae690ccfd7d910126/src/benchmark.cpp)

```
Successfully run the measurement #1.
Total predictions: 32000
Duration time: 15542 ms
Throughput: 2058.94 FPS
Latency: 0.485687 ms
```

```
Successfully run the measurement #2.
Total predictions: 32000
Duration time: 16216 ms
Throughput: 1973.36 FPS
Latency: 0.50675 ms
```

```
Successfully run the measurement #3.
Total predictions: 32000
Duration time: 16299 ms
Throughput: 1963.31 FPS
Latency: 0.509344 ms
```

## Python実装の結果

Python実装のソースコードと実行結果です。

- [python/benchmark.py](https://github.com/shohata/tensorflow_perf/blob/ae4fbcfb6790c2cd8dcb938ae690ccfd7d910126/python/benchmark.py)

```
Successfully run the measurement #1.
Total predictions:  40064
Duration time:      22638.29686 ms
Throughput:          1769.74444 FPS
Latency:                0.56505 ms
```

```
Successfully run the measurement #2.
Total predictions:  40064
Duration time:      22174.23981 ms
Throughput:          1806.78122 FPS
Latency:                0.55347 ms
```

```
Successfully run the measurement #3.
Total predictions:  40064
Duration time:      86710.36790 ms
Throughput:           462.04394 FPS
Latency:                2.16430 ms
```

## 評価結果

以上を踏まえて評価結果を表にまとめました。

| N   | Throughput (C++) | Throughput (Python) | Latency (C++) | Latency (Python) |
| --: | ---------------: | ------------------: | ------------: | ---------------: |
| 1   | 2058.94 FPS      | 1769.74 FPS         | 0.486 ms      | 0.565 ms         |
| 2   | 1973.36 FPS      | 1806.78 FPS         | 0.507 ms      | 0.553 ms         |
| 3   | 1963.31 FPS      | 462.04 FPS          | 0.509 ms      | 2.164 ms         |

期待通りC++実装の方がPython実装を上回る性能を示しました。スループットが評価１では約16%、評価２では約9%、評価３では約425%高い結果を示しました。

意外なのがワーカー数４と１で結果がさほど変わらないことです。C++実装で約4%の向上に留まりました。むしろPython実装ではワーカ数の多い方が、性能が約2%低下する結果となりました。今回の条件の場合、マルチスレッドは大きく貢献しないことがわかりました。

forループでの比較は予想通りC++実装の方が圧倒的に高い結果となりました。Pythonのfor文はオーバーヘッドが大きく遅いことで有名です。その評判通り、Python実装のスループットは約4分の1まで低下しました。高速化においてPythonのfor文がいかに禁忌かがよくわかる結果です。一方でC++実装ではfor文を使ってもほとんどスループットが低下しませんでした。今回の場合、C++実装ではforループでも十分な性能を出せることがわかりました。

## 感想

C++実装の方が高い性能になり喜ばしい反面、思ったより性能差がなくてガックリしました。というのも実装するのがPythonの10倍大変だったからです。資料も少なく手探りの実装で、ジョブシステムも標準ライブラリにないので手作りです。コードの行数も3倍あります。そこまで頑張って16%の性能向上……。10倍とは言わなくても、2倍くらい速くならないかなとか思っていました。Pythonってよくできているんですね。でも、高速化やC++の勉強になったのはよかったです。

## まとめ

今回はC++実装とPython実装で推論処理の性能を比較しました。期待通りC++実装の方が高速化を期待できます。一方でPython実装もうまく書けばC++実装に迫る性能を発揮することがわかりました。極限まで高速化したい方は是非C++実装をお試しください。