はじめに
前回の記事では、
ONNX RuntimeをPythonで使い、推論時間0.13msという結果を得た。
PyTorch (0.20ms) やlibtorch (0.23ms) より速く、推論特化の強みが出た。
今回は同じmodel.onnxを使い、C++のONNX Runtime APIで推論してみる。
Pythonのオーバーヘッドが完全になくなったとき、速度はどう変わるか。
全体の流れ
[PyTorch環境] [ONNX Runtime C++環境]
学習 → model.onnx保存 → 読み込み → 推論
※PyTorchもPythonも不要!
前回と同じmodel.onnxをそのまま使い回せる。
これがONNXの強みだ。
環境構築
.devcontainer/devcontainer.json:
{
"name": "ONNX Runtime C++",
"image": "mcr.microsoft.com/devcontainers/cpp:ubuntu-24.04",
"postCreateCommand": "bash .devcontainer/setup.sh",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cpptools",
"ms-vscode.cmake-tools"
]
}
}
}
.devcontainer/setup.sh:
#!/bin/bash
# ONNX Runtime C++版をダウンロード・展開する
set -e
ORT_VERSION="1.18.0"
ORT_URL="https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/onnxruntime-linux-x64-${ORT_VERSION}.tgz"
DEST="$HOME/onnxruntime"
if [ ! -d "$DEST" ]; then
echo "ONNX Runtimeをダウンロード中..."
wget -q "$ORT_URL" -O /tmp/onnxruntime.tgz
tar -xzf /tmp/onnxruntime.tgz -C "$HOME/"
# mkdirせずに直接mvする(パスがずれないように)
mv "$HOME/onnxruntime-linux-x64-${ORT_VERSION}" "$DEST"
rm /tmp/onnxruntime.tgz
echo "ONNX Runtimeのセットアップ完了: $DEST"
else
echo "ONNX Runtimeは既にセットアップ済みです: $DEST"
fi
ビルド設定
CMakeLists.txt:
cmake_minimum_required(VERSION 3.18)
project(onnx_inference)
set(CMAKE_CXX_STANDARD 17)
# ONNX Runtimeのパス
set(ORT_ROOT "$ENV{HOME}/onnxruntime")
# インクルードパスとライブラリの設定
include_directories(${ORT_ROOT}/include)
link_directories(${ORT_ROOT}/lib)
add_executable(inference inference.cpp)
target_link_libraries(inference onnxruntime)
推論コード
inference.cpp:
#include <onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>
#include <chrono>
#include <random>
int main() {
// ONNX Runtimeの初期化
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "onnx_inference");
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(1);
// モデルの読み込み
Ort::Session session(env, "model.onnx", session_options);
std::cout << "モデルを読み込みました: model.onnx" << std::endl;
// 入出力の名前を取得
Ort::AllocatorWithDefaultOptions allocator;
auto input_name_ptr = session.GetInputNameAllocated(0, allocator);
auto output_name_ptr = session.GetOutputNameAllocated(0, allocator);
const char* input_name = input_name_ptr.get();
const char* output_name = output_name_ptr.get();
// ダミー入力を準備(MNISTと同じサイズ: 1x1x28x28)
std::vector<int64_t> input_shape = {1, 1, 28, 28};
size_t input_size = 1 * 1 * 28 * 28;
std::vector<float> input_data(input_size);
std::mt19937 rng(42);
std::normal_distribution<float> dist(0.0f, 1.0f);
for (auto& v : input_data) v = dist(rng);
auto memory_info = Ort::MemoryInfo::CreateCpu(
OrtArenaAllocator, OrtMemTypeDefault
);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
memory_info,
input_data.data(), input_size,
input_shape.data(), input_shape.size()
);
std::vector<const char*> input_names = {input_name};
std::vector<const char*> output_names = {output_name};
// ウォームアップ(前回と条件を揃える)
session.Run(
Ort::RunOptions{nullptr},
input_names.data(), &input_tensor, 1,
output_names.data(), 1
);
std::cout << "ウォームアップ完了" << std::endl;
// 推論時間を計測(100回平均)
const int N = 100;
auto start = std::chrono::high_resolution_clock::now();
std::vector<Ort::Value> output_tensors;
for (int i = 0; i < N; i++) {
output_tensors = session.Run(
Ort::RunOptions{nullptr},
input_names.data(), &input_tensor, 1,
output_names.data(), 1
);
}
auto end = std::chrono::high_resolution_clock::now();
double elapsed_ms =
std::chrono::duration<double, std::milli>(end - start).count() / N;
// 結果の表示
float* output_data = output_tensors[0].GetTensorMutableData<float>();
int predicted = std::max_element(output_data, output_data + 10) - output_data;
std::cout << "予測クラス: " << predicted << std::endl;
std::cout << "推論時間(" << N << "回平均): " << elapsed_ms << " ms" << std::endl;
return 0;
}
ビルドと実行:
mkdir build && cd build
cmake ..
make
./inference
実行結果・4者比較
実行環境:Ryzen 5 7530U(CPUのみ)、ウォームアップあり、100回平均
| 実装 | 推論時間(100回平均) | Pythonとの比較 |
|---|---|---|
| Python (PyTorch) | 0.20 ms | 基準 |
| C++ (libtorch) | 0.23 ms | 1.15倍遅い |
| ONNX Runtime (Python) | 0.13 ms | 1.54倍速い |
| ONNX Runtime (C++) | 0.04 ms | 5倍速い! |
圧倒的な差が出た。
なぜここまで速いのか
同じmodel.onnxを使っているのに、PythonとC++でこれほど差が出る理由は何か。
Pythonオーバーヘッドの排除
Python版はループのたびにインタープリタを介する。
C++版はそのオーバーヘッドが完全にゼロだ。
ONNX Runtimeのグラフ最適化
ONNX Runtimeは推論に特化した最適化を自動で行う。
C++環境ではその最適化が余すことなく効く。
メモリ管理の効率化
PythonはGCの影響を受けるが、C++では直接制御できる。
100回ループでのメモリ割り当て・解放のコストが大きく違う。
まとめ
4回の比較を振り返ると:
| 実装 | 速度 | 主なユースケース |
|---|---|---|
| Python (PyTorch) | 0.20 ms | 学習・開発・検証 |
| C++ (libtorch) | 0.23 ms | Pythonが使えない環境 |
| ONNX Runtime (Python) | 0.13 ms | 軽量・クロス言語デプロイ |
| ONNX Runtime (C++) | 0.04 ms | 速度重視の本番環境 |
速度だけを追うならONNX Runtime (C++) が圧倒的だ。
ただし開発・検証のしやすさを考えるとPython (PyTorch) が現実的な選択肢になる。
「何のために推論するか」でツールを選ぶのが正解だと思う。
おわりに・次回予告
次回はONNX Runtime (C#) での推論を試す予定。
同じmodel.onnxがC#でも使えるか、速度はどう変わるか確認していく。
NuGetで簡単にインストールできる点も見どころだ。