C++とPythonのコラボレーション

SWIG連携による実行速度効率化

tags:python, tips, experiment
created:2006-03-31T10:00:14

SWIG連携による実行速度効率化(Python25対応) PythonはSWIGとの連携をサポートする機能が標準で付属されているのです!

概要

「SWIG」というC/C++コードを各種言語から 利用できるようにするラッパージェネレータがあるのをご存知でしょうか?

ホームサイト: http://www.swig.org/

  • Allegro CL
  • C#
  • Chicken
  • Guile
  • Java
  • Modula-3
  • Mzscheme
  • OCAML
  • Perl
  • PHP
  • python
  • Ruby
  • Tcl

といった多彩なジェネレート出力が得られます。

このページでは当然Pythonからの利用を解説しますが、 実はPython標準ディストリビューションには SWIGとの連携をサポートする機能が最初から組み込み済みです。

つまり、他の言語利用よりもPythonとSWIGの組み合わせが最も親和性が高いといえます。

PythonとSWIGの連携実績は以下のように非常に有名なプロジェクトがあることもポイントですね。

  • wxPython
  • PyOgre
  • PyOpenGL

SWIGとの連携により以下のようなメリットが得られます。

  1. C/C++の膨大な資産をPythonから利用出来るようになる
  2. 速度の必要な部分をネイティブコードで作成することで実行速度の効率化

で、実行速度が気にならない部分はPythonでごりごり作ってしまえば、 すべてC/C++で作成するのに比べてかなり早くアプリケーション構築が出来るでしょう。

ただ、残念ながら1.は手放しでオッケーというわけではないのです。 SWIGはC/C++のヘッダーのほとんどを理解できますが、 PythonとC/C++の仕様の違いを完全に埋めることが出来ません。

最もわかりやすいのは、C++のメソッドオーバーロードでしょう。 Pythonのメソッド引数には型を明示する必要がないため、 引数の型でどのメソッドを呼ぶのかを決める メソッドオーバーロードがPythonでは実現できません。

SWIGではインターフェース定義に別名を明示することで解決する方法を提供しています。 wxPythonがそういった解決方法によりラッパーをビルドしています。

一度明示したインターフェース定義が作成できれば以後のビルドはオートですので、 wxWidgetsのバージョンアップからwxPythonのリリースまでの タイムラグが短いのもSWIGとPythonの連携がうまくいっている良い例でしょう。

ここでは、C++とPythonの連携方法を解説します。

あらかじめ必要なソフトウェア

SWIG本体:

現時点(2007/03)でのWindows用最新版バイナリ

swigwin-1.3.31.zip

C++コンパイラ:
VisualC++コンパイラまたはMingw32コンパイラ

筆者はMingw32が内臓された CodeBlocks を愛用しています。

フォルダ構成

  • C:\(ドライブルート)
    • Program Files
      • CodeBlocks
        • bin
          • g++.exe
          • 他いろいろ
        • 他いろいろ
    • swigwin
      • swig.exe
      • 他いろいろ

必要な環境変数設定

INCLUDE=C:\Program Files\CodeBlocks\include;C:\Program Files\CodeBlocks\mingw32\include
PATH=C:\Program Files\CodeBlocks\bin;C:\swigwin;%PATH%

VCコンパイラを持っているブルジョアは VCのコンパイラ用の環境変数設定を行ってください。

一時的でよければ、vsvars32.batかvcvars32.batとかいうバッチファイルが MSビジュアルスタジオインストールフォルダの どこかにあるでしょうからそれを実行してください。

サンプル

ファイル構成

  • sample(フォルダ)
    • sample.i
    • sample.h
    • sample.cpp
    • setup.py
sample.i
%module sample

%{
#include "sample.h"
%}

%include "sample.h"
sample.h
class Sample {
private:
  bool is_open;
public:
  Sample();
  ~Sample();
  void open();
  void close();
  int read();
  void write(int data);
};
sample.cpp
#include <stdio.h>
#include "sample.h"

Sample::Sample()
 : is_open(false)
{
  printf("create this device!\n");
}

Sample::~Sample()
{
  close();
  printf("destroy this device!\n");
}

void Sample::open()
{
  if (!is_open)
  {
    is_open = true;
    printf("open this device!\n");
  }
}

void Sample::close()
{
  if(is_open)
  {
    is_open = false;
    printf("close this device!\n");
  }
}

int Sample::read()
{
  printf("read this device!\n");
  return 0;
}

void Sample::write(int data)
{
  printf("write this device!(value=%d)\n", data);
}
setup.py
from distutils.core import setup, Extension

setup(
  ext_modules = [
    Extension('_sample', ['sample.i', 'sample.cpp'],
      library_dirs=[],
      libraries=[],
      extra_compile_args=[],
      extra_link_args=[])
  ])

ライブラリフォルダの指定や、別途リンクする必要のあるライブラリはここで指定しておきます。

mingw32で、ライブラリにsampleフォルダにある「libhoge.a」ライブラリをリンクしたいなら、

from distutils.core import setup, Extension

setup(
  ext_modules = [
    Extension('_sample', ['sample.i', 'sample.cpp'],
      library_dirs=['.'],
      libraries=['hoge'],
      extra_compile_args=[],
      extra_link_args=[])
  ])

というように指定します。

mingw32では、ライブラリ名を指定した時、 参照するファイル名は「lib」プレフィクスがつき、拡張子は「.a」になります。

VCでは、普通にファイル名を指定してください。

ビルド方法

前述の環境変数設定が行われた状態のコマンドラインから

setup.py build_ext --swig-cpp -c mingw32

とすることでSWIGジェネレータの起動、コンパイル、リンクまでを一気に行います。

どうですか?簡単でしょう!

エラーがあるとその時点で停止します。

VCコンパイラを持っているブルジョアは

「-c mingw32」

オプションをなくしてください。

うちでの出力は以下のような内容になりました。

running build_ext
building '_sample' extension
--swig-cpp is deprecated - use --swig-opts=-c++
swigging sample.i to sample_wrap.cpp
C:\swigwin\swig.exe -python -c++ -o sample_wrap.cpp sample.i
creating build
creating build\temp.win32-2.5
creating build\temp.win32-2.5\Release
C:\Program Files\CodeBlocks\bin\gcc.exe -mno-cygwin -mdll -O -Wall -IC:\Python25\include -IC:\Python25\PC -c sample_wrap.cpp -o build\temp.win32-2.5\Release\sample_wrap.o
C:\Program Files\CodeBlocks\bin\gcc.exe -mno-cygwin -mdll -O -Wall -IC:\Python25\include -IC:\Python25\PC -c sample.cpp -o build\temp.win32-2.5\Release\sample.o
writing build\temp.win32-2.5\Release\_sample.def
creating build\lib.win32-2.5
C:\Program Files\CodeBlocks\bin\g++.exe -mno-cygwin -mdll -static --entry _DllMain@12 -o build\temp.win32-2.5\Release\lib_sample.a --def build\temp.win32-2.5\Release\_sample.def -s build\temp.win32-2.5\Release\sample_wrap.o build\temp.win32-2.5\Release\sample.o -LC:\Python25\libs -LC:\Python25\PCBuild -lpython25 -lmsvcr71 -o build\lib.win32-2.5\_sample.pyd

注釈

lib_sample.aが見つからないというエラーが出る場合、 C:\Python25\Lib\distutils\cygwincompiler.pyの211行目あたりの 「–output-lib」を「-o」に変えてみてください。

ビルドログ中のコマンドが以下のように変更されます。

–output-lib build\...\lib_sample.a

-o build\...\lib_sample.a

mingw32のバージョンにも依るのかも?

生成されるもの

  • sample.py
  • sample_wrap.cpp
  • build(フォルダ)
    • lib.win32-2.5(フォルダ)
      • _sample.pyd

利用方法

sample.pyと_sample.pydを同じフォルダにおいて

pythonのインタラクティブモードを起動して動作確認してみましょう。

>>> import sample
>>> dev = sample.Sample()
create this device!
>>> dev.open()
open this device!
>>> dev.open()
>>> dev.close()
close this device!
>>> dev.close()
>>> dev.read()
read this device!
0
>>> dev.write(123)
write this device!(value=123)
>>> del dev
destroy this device!
>>>

こんな感じで、C++クラスがそのままPythonから利用できることがわかります。

結論

これらのSWIGによるC++とPythonの連携を活用することで、 作成できるアプリケーションの範囲が大幅に広がることがわかるでしょう。

追記

他にもネイティブコードとの連携を支援するツールはいろいろあります。

  • Py++
  • Cython(旧Pyrex)
  • boost.python
  • SciPy.weave
  • ctypes/ctypeslib
  • etc..

どれを使ってもPythonオブジェクトとネイティブオブジェクトの変換コストが 大きなボトルネックになります。 ネイティブ側で処理量がある程度のカタマリになるような設計が重要になります。

イテレータのようなミニマムな処理量で Pythonとネイティブを行ったり来たりするなどしてしまうと Pythonのみのコードのほうが早かったりします。

どのツールを選ぶか重要なポイントは以下のどちらかということ。

  • C/C++資産の活用が目的(旧コードを活かす)
  • 高速な処理の実現が目的(新規にコードを起こす)

前者に向いているのはPy++、ctypesなど。 後者に向いているのはCythonやweave、boostなど。

だけどCのみで頑張って書かれたコードで3時間の処理が numpyベースに移植したら1時間になったりもする。 ネイティブだから早いかも?っていう目測はたまに外れることがあるのです。

気をつけましょう(自戒を込めて)