本記事では、PythonとC++を連携する雛形コードを載せました。具体的には、データを抽出するためのプログラムで、forループとその中にあるif文条件分岐の部分をC++に任せて高速化を図ります。仕組みは、C++でPythonモジュールを作成することでPythonスクリプト内でインポートし、それに引数を与えて処理結果を戻り値で受け取ることが出来ます。
※Linux(Ubuntu)で動作確認しています。Windowsに関してはCMakeLists.txtのコードのみ文末に載せます。(PythonとC++の連携方法は、Visual Stadio 2019やVS codeを使用する手段もあります。本記事ではそれには触れていません)
はじめに、高速化度合いを検証した結果が下図です。
(検証内容の詳細は後述しますが)入力因子の「乱数で生成した数値の範囲」が少ない100の場合の処理に掛かった時間を比較すると、C++(pybind11)の場合は0.01分(0.34秒)、numpyの場合は0.38分(22.79秒)、listの場合は0.06分(3.62秒)です。つまり、C++(pybind11)が、順に38倍、6倍速い結果です。
しかし、扱うデータ数が多い1万の場合の処理に掛かった時間は、C++(pybind11)の場合は0.42分(25.42秒)、numpyの場合は0.47分(28.09秒)、listの場合は3.94分(236.17秒)です。つまり、C++(pybind11)が、順に1.1倍、561倍速い結果です。
以上より、少なくとも扱うデータによってご利益の度合いに差があることがわかりました。下図は、それをグラフ化した折れ線図です。興味深いのが、横軸のデータ数1万を超えた辺りからC++(pybind11)はnumpyにより遅くなっています。forループ以外の何かが律速(オーバーヘッド)している可能性があります(例えばif文条件分岐によるデータ抽出部分)
結局、実務において重要なのは体感できる程の速度差があるのか?です。
上図の検証内容の詳細について説明します。次のような4列の35万行のデータがあります。各セルの数字は乱数で生成しています。この乱数生成の範囲が上図の横軸です。つまり扱うデータ数の多さをパラメータとしています。
そして、下図のように数字がまた無数にあります。これもまた乱数で作成したもので、生成範囲は上図と同じです。数字に重複はありません。ここで、上図の各行を一行ずつforループで回した時に、下図にある数字が4列全て存在すれば、その行番号(インデックス)を取得してゆくプログラムです。この特徴は、forループが複数あるのと、if文による条件分岐があります。この処理に掛かった時間をC++(pybind11)とnumpy、listでした3つの場合で比較検証したのが冒頭のグラフでした。
今回の検証の結果、C++のコード作成とコンパイルのためのトライ&エラーをする時間も加味すると、ループ処理のためにPythonとC++を連携使用するのは必ずしも適切とは言えない。つまり、総合的早さを考慮すると、numpyで十分でありえる場合もあるとわかった。
■インストール
作業1. pybind11を入れる。condaでもpipでも出来る
conda install -c conda-forge pybind11
pip install pybind11
作業2. cmakeを入れる。
cmake.org
作業3. C++のコンパイラであるg++やmakeに関連するツールを入れる。
sudo apt-get install build-essential
■本プログラム
次の①~③の3つのコードが必要です。そして、buildディレクトリを作成して、cd(チェンジディレクトリ)でそこに移動して次の3つの順でコマンド実行でできます。
上から順に、cmake ..コマンドによりコンパイルするためのスクリプトファイルMakefileを作成。makeコマンドで.soファイルを作成。最後にpythonスクリプトで実行。
$ cmake ..
$ make
$ python3 main_script.py
▼①. C++のコード
繰り返しループ処理の部分をcppで書く。この時、Pythonライブラリのpybind11というヘッダーを利用する。これをコンパイルすると、.soファイルが生成される(Windowsの場合は.prd)。これは、Pythonモジュールであって、Pythonスクリプト内でインポートすることで関数のように呼び出せるようになる。ファイル名はここでは「loop.cpp」としています。
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <iostream.h>
using namespace std;
namespace py = pybind11;
py::array_t<int> extract_rows_index(
py::array_t<double> input1,
py::array_t<double> input2,
int elem)
{
py::buffer_info buf1 = input1.request(), buf2 = input2.request();
if (buf1.ndim != 2 || buf2.ndim != 1)
throw std::runtime_error("Number of dimensions must be one");
auto r = input1.unchecked<2>();
double *ptr2 = static_cast<double *>(buf2.ptr);
py::array_t<double> result_array({elem});
auto r2 = result_array.mutable_unchecked<1>();
int n = 0;
int flag_cnt;
for (int i = 0; i < r.shape(0); i++){
flag_cnt = 0;
for (int j = 0; j < r.shape(1); j++){
for (int k = 0; k < buf2.shape[0]; k++){
if (r(i, j) == ptr2[k] ){
flag_cnt += 1;
}
}
}
if (flag_cnt == 4){
r2(n) = i;
n += 1;
}
}
return result_array;
}
PYBIND11_MODULE(loop, m) {
m.def("extract_rows_index", &extract_rows_index, "comment aiueo");
}
▼②. CMakeのコード
ファイル名は「CMakeLists.txt」とする必要あります。ビルド自動化を目的としたツールです。OSや、コンパイラが異なっても設定が一目にわかり、調整がし易いです。(例えば、WindowsとLinuxではコンパイラの種類やver、パスが異なるため、これがあると調整がし易いです)
cmake_minimum_required(VERSION 3.16)
project(aaa VERSION 0.1.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "-O1 -Wall")
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g")
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-Og -g")
find_package(pybind11 REQUIRED)
find_package(Python REQUIRED)
message(${Python_FOUND})
message(${Python_EXECUTABLE})
include_directories(/home/[userid]/anaconda3/envs/py37/include)
include_directories(/home/[userid]/anaconda3/envs/py37/include/pybind11)
link_directories(/home/[userid]/anaconda3/envs/py37/lib)
pybind11_add_module(loop loop.cpp)
▼③. Pythonのコード
ファイル名は例えば「main_script.py」とする。データの生成やグラフ化などの入出処理はPythonを使います。C++で作成したPythonモジュールのインポートの例は、ここではfrom loop import extract_rows_index です。
import pandas as pd
import numpy as np
from loop import extract_rows_index
import time
from tqdm import tqdm
from numpy.random import *
random_max_value = 5000
original_data_rows = 350000
num_of_target_extract = int(random_max_value / 2)
np_array_list = []
for i in range(4):
np_array_list.append(randint(0, random_max_value, original_data_rows))
array_list = [np_array.tolist() for np_array in np_array_list]
df = pd.DataFrame(array_list).T
df.columns = ['node1', 'node2', 'node3', 'node4']
node_data_list = df[['node1', 'node2', 'node3', 'node4']].values.tolist()
node_data_np = np.array(node_data_list)
target_list = randint(0, random_max_value, num_of_target_extract).tolist()
target_node_list = list(set(target_list))
target_node_list.sort()
df2 = pd.DataFrame(target_node_list)
target_node_np = np.array(target_node_list)
print(r'########################################### c++')
start = time.time()
arr = extract_rows_index(node_data_np,
target_node_np,
int(len(node_data_np)/10))
for i, t in enumerate(arr):
if(i > 0) and (t == 0):
buf = i
break
arr2 = np.resize(arr, buf)
extract_rows_No_list = arr2.tolist()
print(extract_rows_No_list[:10])
print(extract_rows_No_list[-10:])
print(type(extract_rows_No_list))
cpp_time = time.time() - start
print("c++ time -> ", cpp_time)
print(len(extract_rows_No_list), '/', len(node_data_list))
print(r'########################################### numpy')
extract_rows_No_list = []
extract_rows_data_list = []
start = time.time()
for i, row in tqdm(enumerate(node_data_np)):
cnt = 0
row_data_list = []
for data in row:
if data in target_node_np:
row_data_list.append(data)
cnt += 1
if cnt==4:
extract_rows_No_list.append(i)
extract_rows_data_list.append(row_data_list)
print(extract_rows_No_list[:10])
print(extract_rows_No_list[-10:])
print(type(extract_rows_No_list))
numpy_time = time.time() - start
print("numpy time -> ", numpy_time)
print(len(extract_rows_No_list), '/', len(node_data_list))
print(r'########################################### list')
extract_rows_No_list = []
extract_rows_data_list = []
start = time.time()
for i, row in tqdm(enumerate(node_data_list)):
cnt = 0
row_data_list = []
for data in row:
if data in target_node_list:
row_data_list.append(data)
cnt += 1
if cnt==4:
extract_rows_No_list.append(i)
extract_rows_data_list.append(row_data_list)
print(extract_rows_No_list[:10])
print(extract_rows_No_list[-10:])
print(type(extract_rows_No_list))
list_time = time.time() - start
print("list time -> ", list_time)
print(len(extract_rows_No_list), '/', len(node_data_list))
data_list = [random_max_value, original_data_rows, cpp_time, numpy_time, list_time]
data_list = list(map(str, data_list))
data_str = ','.join(data_list)
with open('00_time_data.csv', 'a') as f:
f.write(data_str + '\n')
print("finished")
#################################
▼(Windows)CMakeのコード
C++コンパイラのg++にMinGWを使用する場合、64bitPCでは64bitをインストールして使用すること
cmake_minimum_required(VERSION 3.19)
project(aaaa CXX)
include(FetchContent)
FetchContent_Declare(
pybind11
GIT_REPOSITORY https://github.com/pybind/pybind11
GIT_TAG v2.6.1
)
FetchContent_MakeAvailable(pybind11)
set(CMAKE_CXX_COMPILER C:\\mingw-w64\\mingw64\\bin)
set(CMAKE_CXX_FLAGS "-Wall -std=c++1z")
find_package(Python REQUIRED)
message(${Python_FOUND})
message(${Python_EXECUTABLE})
include_directories(C:\\Users\\[userid]\\Anaconda3\\include)
include_directories(C:\\Users\\[userid]\\Anaconda3\\lib\\site-packages\\pybind11\\include)
include_directories(C:\\Users\\[userid]\\Anaconda3\\Lib\\site-packages\\pybind11\\include\\pybind11)
link_directories(C:\\Users\\[userid]\\Anaconda3\\Lib)
pybind11_add_module(main main.cpp)
実行例を下記に記す(Linuxの場合とは異なります)
$ make .. -G "MinGW Makefiles"
$ mingw32-make loop
$ python main_script.py
以上
<広告>
リンク