📆

C++で2値化処理を高速化してOpenCVと比較してみた

に公開2

C++で2値化処理を実装し、処理速度の比較を行いました。
今回はOpenCV標準実装(cv::threshold)と自作実装(分岐あり/分岐なし/LUT/SIMD + OpenMP)を比較します。

概要

画像の2値化処理は単純に見えますが、
実装方法によって 実行時間に大きな差が生じます。

本プロジェクトでは以下の実装を比較します

  • 分岐あり(三項演算子)
  • 分岐なし実装
  • LUT (ルックアップテーブル)
  • SIMD + OpenMP (自作)
  • OpenCV (cv::threshold)

AVX2有効環境での実測結果を通して、
CPU最適化がどこまで効くのかを検証します。

実行環境

  • CPU: Intel Core Ultra 7 265KF
  • OS: Windows 11
  • Compiler: MSVC 19.50
  • Build: Release
  • SIMD: AVX2
  • Parallel: OpenMP

プロジェクト

ソースコードを含むプロジェクトはこちらにアップロードしました。
https://github.com/shimon0724/2BinProcessSpeedComp

ビルド方法

1. vcpkgの準備

git clone https://github.com/microsoft/vcpkg.git C:/tools/vcpkg
cd C:/tools/vcpkg
bootstrap-vcpkg.bat

※ C:/tools/vcpkg は例です。
別の場所に置く場合は後述のCMAKE_TOOLCHAIN_FILEを変更してください。

2. リポジトリのクローン

git clone https://github.com/yourname/2BinProcessSpeedComp.git
cd 2BinProcessSpeedComp

3. CMake Presetを使用してビルド

Visual Studio を使う場合はリポジトリのルートフォルダを開き、
vcpkg-x64-release
を選択してビルドを開始してください。
※ 初回ビルド時にvcpkgが自動でOpenCVをインストールします。
※ .slnまたは.slnxファイルは作成されません(CMakeプロジェクトの正常な挙動です)

4. コマンドラインでビルドする場合

cmake -S . -B out/build/vcpkg-x64-release -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=C:/tools/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build out/build/vcpkg-x64-release

5. 実行

out/build/vcpkg-x64-release/2BinProcessSpeedComp/2BinProcessSpeedComp.exe

vcpkgが見つからない場合

CMAKE_TOOLCHAIN_FILEのパスを自分のvcpkgの場所に合わせて修正してください。

CMAKE_TOOLCHAIN_FILE not found

実行結果

AVX2 enabled
[分岐あり]    629256700 ns
[分岐なし]   646292400 ns
[LUT]       479005500 ns
[SIMD+OpenMP] 32230600 ns
[OpenCV]      45822200 ns
sink=623475

考察

SIMD+OpenMPが最速という結果になりました。
OpenCVが最も早いと思っていましたが、自作のほうが早かったのは意外でした。
OpenCVは汎用ライブラリとして多様な入力条件・型に対応するための処理が含まれています。
一方自作実装では条件を限定することで分岐や汎用処理を排除できたため、この差が性能に表れたものと推測します。

詳細はこちら

https://shimons-labo.com/opencvのcvthresholdはなぜ高速なのか自前の2値化を最適化して/

Discussion

藤田望藤田望
[SIMD+OpenMP] 32230600 ns
[OpenCV]      45822200 ns

↑に対して

[分岐あり]    629256700 ns
[分岐なし]   646292400 ns
[LUT]       479005500 ns

↑が桁違いに遅いのは自動ベクター化が働いていないのが大きい気がします。

[LUT]       479005500 ns

↑に対して

[分岐あり]    629256700 ns
[分岐なし]   646292400 ns

↑で1.3倍位になってるのも適正に最適化指示がされてればそんな差が出るはずがない気がして違和感あるのでコンパイラへの最適化指示を確認された上でコンパイラの出力コードを確認されると良いと思います。その辺りされないで「なんか速かった(遅かった)」という感想レベルで終わらせてしまうのはもったいないと思いますね。

shimonshimon

コメントありがとうございます!

ご指摘の通り、分岐あり/なし/LUTについては自動ベクター化が効いていない可能性が高そうです。

今回の検証はアルゴリズムの差による比較でしたがコンパイラの最適化まで深堀すれば違った結果が見えてくるかもしれません。

次回はその観点でも検証してみたいです!