setuptools最新はまだCythonのためならず

Microsoft Visual C++ Compiler for Python 2.7はひとのためならずで大喜びしたのも束の間。

Windows で Cython はもう手が届かない夢ではない

そうなんだけれども…

Microsoft Visual C++ Compiler for Python 2.7はひとのためならずの本当の狙いは「Cython」であったりした。で、さぁ、と思ってハメられた。

よくよく考えてみれば当然だが「setuptools」を最新にしたからといって、自動的にPython標準添付の distutils がさしかわるわけではない。

こんな setup.py:

setup.py
 1 # -*- coding: utf-8 -*-
 2 # -----------------------------------
 3 from distutils.core import setup
 4 from distutils.extension import Extension
 5 from Cython.Distutils import build_ext
 6 from Cython.Build import cythonize
 7 # .........................................
 8 from distutils.sysconfig import get_python_inc
 9 import sys
10 from os import path
11 include_dirs = [
12     get_python_inc(plat_specific=1),
13     "src",
14 ]
15 library_dirs = [
16 ]
17 libraries = [
18 ]
19 # .........................................
20 setup(
21     cmdclass = {'build_ext': build_ext},
22     #...省略...
23 )
24 # .........................................

は、

1 me@host ~$ python setup.py build_ext

で「vcvarsall.batが見つからない」と、相変わらず言われる。

どこかの Python 本体のアップデートで更新される(あるいはCythonがsetuptools依存になる)ことを期待しているが、当座手で直接 distutils を編集してやった。c:/Python27/Lib/distutils に msvc9compiler.py があるはずである。その def find_vcvarsall(version): をこのようにする:

c:/Python27/Lib/distutils/msvc9compiler.py
 1 def find_vcvarsall(version):
 2     """Find the vcvarsall.bat file
 3 
 4     At first it tries to find the productdir of VS 2008 in the registry. If
 5     that fails it falls back to the VS90COMNTOOLS env var.
 6     """
 7     # 追加BEGIN
 8     VC_BASE = r'Software\%sMicrosoft\DevDiv\VCForPython\%0.1f'
 9     key = VC_BASE % ('', version)
10     try:
11         # Per-user installs register the compiler path here
12         productdir = Reg.get_value(key, "installdir")
13     except KeyError:
14         try:
15             # All-user installs on a 64-bit system register here
16             key = VC_BASE % ('Wow6432Node\\', version)
17             productdir = Reg.get_value(key, "installdir")
18         except KeyError:
19             productdir = None
20     # 追加END
21 
22     # 変更BEGIN
23     # 以下部分オリジナルは if not productdir: でない 
24     if not productdir:
25         vsbase = VS_BASE % version
26         try:
27             productdir = Reg.get_value(r"%s\Setup\VC" % vsbase,
28                                    "productdir")
29         except KeyError:
30             productdir = None
31     # 変更END

(2015-05-20 追記: オリジナルのファイルのファイルエンコーディング指定がないので、日本語コメントは入れないでね。)

気を取り直して

Windows での Cython は、これまで敷居が高かったと思う。でも「Microsoft Visual C++ Compiler for Python 2.7」があればもう大丈夫、上の措置をしておけば…。

今更 Cython について「紹介」するのは季節はずれもいいところだが、とはいえ Cython について検索してみても、「完全でシンプルな」サンプルはそんなにないように思う。実際は「OSS で実際に使っているもの」が山ほどあるので、それを見れば良いのだが、これだと「複雑すぎる」ことになり、とっつき難いかと思う。なので、ちょっと2時間ほどで簡単な例をでっちあげてみた。

この記事全体で使ったサンプルは
jenkins_spooky_hash_as_just_for_cython_example.zipからダウンロード出来るので、参考にどうぞ。ただし、あくまでも「Cythonのサンプル」として書いただけで、テストも何もしてないので、自己責任でご利用ください。

Cython の Windows 版を入手する

Windows で Cython、の記事のつもりなので。

setuptoolsはインストール済み、として、

1 me@host: ~$ easy_install pip
2 me@host: ~$ pip install wheel

http://www.lfd.uci.edu/~gohlke/pythonlibs/#cython より Cython‑0.22‑cp27‑none‑win_amd64.whl (等貴方の環境に合ったもの)を入手、

1 me@host: ~$ pip install Cython‑0.22‑cp27‑none‑win_amd64.whl

Cythonの基礎を説明しだすとキリがないのでコード貼り付け攻撃

Pythonの知識、C の知識、Python の「埋め込み」の知識、Python C API の知識、そして今回の場合は C++ の知識、と、ただでさえ色んな基礎が必要な上に「Cython」という「Python と C のチャンポン言語」、となれば、「お気楽ご気楽Cython」には、もとよりなりえない。やはりある程度の知識がなく飛び込むのは多分無謀だろう。

Cython の使い方には簡単なほうと難しいほうがあって、「元々 pure Python で少々速度に不満があったものを、ちょいと Cython で高速化」は結構「お気楽ご気楽」である。けれど、今回例にするのは、「C++ コードをダイレクトに利用する」方で、難易度が高い方の Cython。

なお、C を呼び出すだけなら ctypes でも良いし、SWIG も選択肢となる。ctypes はむしろ簡単ではない(独特すぎる)けれども pure Python であるし、「C/C++との糊付け」を手軽にやりたければ、SWIG の方が圧倒的に簡単である。

今回例にするのは、Bob Jenkins の「Spooky Hash」にしてみた。多少の難易度があるサンプルでないと、「なんだ簡単じゃん」とわかったつもりになって、実用的に使おうとして一気に落ち込むことになるであろうし。

さて、貼り付け攻撃だ。

まずはパッケージ構成とフォルダ構成を決めて、setup.py をささっと書いてしまうのがセオリーと思う。構成はこんなふうにしてみた:

1 ./lib/jenkins_hash/spooky.pxd
2 ./lib/jenkins_hash/spooky.pyx
3 ./lib/jenkins_hash/__init__.py
4 ./setup.py
5 ./src/SpookyV2.cpp
6 ./src/SpookyV2.h
7 ./src/TestSpookyV2.cpp

srcの下のものは、Bob Jenkins のサイトから持ってきたままの、そのままで、何一つ書き換えない。

setup.pyは:

./setup.py
 1 # -*- coding: utf-8 -*-
 2 # -----------------------------------
 3 # This is just for explanation of Cython in http://hhsprings.pinoko.jp/site-hhs/,
 4 # thus, DO NOT USE FOR THE PURPOSE OF OFFICIAL USAGE, AND DO NOT TRUST ME.
 5 from distutils.core import setup
 6 from distutils.extension import Extension
 7 from Cython.Distutils import build_ext
 8 from Cython.Build import cythonize
 9 # .........................................
10 from distutils.sysconfig import get_python_inc
11 import sys
12 from os import path
13 include_dirs = [
14     get_python_inc(plat_specific=1),
15     "src",
16 ]
17 library_dirs = [
18 ]
19 libraries = [
20 ]
21 # .........................................
22 setup(
23     cmdclass = {'build_ext': build_ext},
24     packages = ['jenkins_hash'],
25     package_dir = {'jenkins_hash': 'lib/jenkins_hash'},
26     ext_modules = [
27         Extension(
28             "jenkins_hash.spooky",
29             [
30                 "lib/jenkins_hash/spooky.pxd",
31                 "lib/jenkins_hash/spooky.pyx",
32                 "src/SpookyV2.cpp",
33                 ],
34             include_dirs=include_dirs,
35             library_dirs=library_dirs,
36             libraries=libraries,
37             language="c++")]
38 )
39 # .........................................

という具合。

一応「基礎」としては、「pxd」は、C/C++ で言うところの「ヘッダファイル」に近いもの、「pyx」が Python モジュール本体の「実装ファイル」ということになる。「pxd」は必須ではないが、私は pxd と pyx を書くスタイルが好きである。(pxd は他の Cython プロジェクトが「cimport」するのに使えるのでその目的で「配布」出来る。)

spooky.pxd はこんな感じになった:

./lib/jenkins_hash/spooky.pxd
  1 # -*- coding: utf-8 -*-
  2 # This is just for explanation of Cython in http://hhsprings.pinoko.jp/site-hhs/,
  3 # thus, DO NOT USE FOR THE PURPOSE OF OFFICIAL USAGE, AND DO NOT TRUST ME.
  4 ctypedef unsigned long long uint64
  5 ctypedef unsigned long uint32
  6 
  7 cdef extern from "<SpookyV2.h>":
  8 
  9     cdef cppclass SpookyHash:
 10 
 11         #
 12         # SpookyHash: hash a single message in one call, produce 128-bit output
 13         #
 14         @staticmethod
 15         void Hash128(
 16             const void *message,  # message to hash
 17             size_t length,        # length of message in bytes
 18             uint64 *hash1,        # in/out: in seed 1, out hash value 1
 19             uint64 *hash2) nogil  # in/out: in seed 2, out hash value 2
 20 
 21         #
 22         # Hash64: hash a single message in one call, return 64-bit output
 23         #
 24         @staticmethod
 25         uint64 Hash64(
 26             const void *message,  # message to hash
 27             size_t length,        # length of message in bytes
 28             uint64 seed) nogil    # seed
 29 
 30         #
 31         # Hash32: hash a single message in one call, produce 32-bit output
 32         #
 33         @staticmethod
 34         uint32 Hash32(
 35             const void *message,  # message to hash
 36             size_t length,        # length of message in bytes
 37             uint32 seed) nogil    # seed
 38 
 39         #
 40         # Init: initialize the context of a SpookyHash
 41         #
 42         void Init(
 43             uint64 seed1,       # any 64-bit value will do, including 0
 44             uint64 seed2) nogil # different seeds produce independent hashes
 45 
 46         #
 47         # Update: add a piece of a message to a SpookyHash state
 48         #
 49         void Update(
 50             const void *message,  # message fragment
 51             size_t length) nogil  # length of message fragment in bytes
 52 
 53         #
 54         # Final: compute the hash for the current SpookyHash state
 55         #
 56         # This does not modify the state; you can keep updating it afterward
 57         #
 58         # The result is the same as if SpookyHash() had been called with
 59         # all the pieces concatenated into one message.
 60         #
 61         void Final(
 62             uint64 *hash1,       # out only: first 64 bits of hash value.
 63             uint64 *hash2) nogil # out only: second 64 bits of hash value.
 64 
 65         ##
 66         ## left rotate a 64-bit value by k bytes
 67         ##
 68         #@staticmethod
 69         #uint64 Rot64(uint64 x, int k) nogil
 70 
 71         ##
 72         ## This is used if the input is 96 bytes long or longer.
 73         ##
 74         ## The internal state is fully overwritten every 96 bytes.
 75         ## Every input bit appears to cause at least 128 bits of entropy
 76         ## before 96 other bytes are combined, when run forward or backward
 77         ##   For every input bit,
 78         ##   Two inputs differing in just that input bit
 79         ##   Where "differ" means xor or subtraction
 80         ##   And the base value is random
 81         ##   When run forward or backwards one Mix
 82         ## I tried 3 pairs of each; they all differed by at least 212 bits.
 83         ##
 84         #@staticmethod
 85         #void Mix(
 86         #    const uint64 *data, 
 87         #    uint64& s0, uint64& s1, uint64& s2, uint64& s3,
 88         #    uint64& s4, uint64& s5, uint64& s6, uint64& s7,
 89         #    uint64& s8, uint64& s9, uint64& s10, uint64& s11) nogil
 90 
 91         ##
 92         ## Mix all 12 inputs together so that h0, h1 are a hash of them all.
 93         ##
 94         ## For two inputs differing in just the input bits
 95         ## Where "differ" means xor or subtraction
 96         ## And the base value is random, or a counting value starting at that bit
 97         ## The final result will have each bit of h0, h1 flip
 98         ## For every input bit,
 99         ## with probability 50 +- .3%
100         ## For every pair of input bits,
101         ## with probability 50 +- 3%
102         ##
103         ## This does not rely on the last Mix() call having already mixed some.
104         ## Two iterations was almost good enough for a 64-bit result, but a
105         ## 128-bit result is reported, so End() does three iterations.
106         ##
107         #@staticmethod
108         #void EndPartial(
109         #    uint64& h0, uint64& h1, uint64& h2, uint64& h3,
110         #    uint64& h4, uint64& h5, uint64& h6, uint64& h7, 
111         #    uint64& h8, uint64& h9, uint64& h10, uint64& h11) nogil
112 
113         #@staticmethod
114         #void End(
115         #    const uint64 *data, 
116         #    uint64& h0, uint64& h1, uint64& h2, uint64& h3,
117         #    uint64& h4, uint64& h5, uint64& h6, uint64& h7, 
118         #    uint64& h8, uint64& h9, uint64& h10, uint64& h11) nogil
119 
120         ##
121         ## The goal is for each bit of the input to expand into 128 bits of 
122         ##   apparent entropy before it is fully overwritten.
123         ## n trials both set and cleared at least m bits of h0 h1 h2 h3
124         ##   n: 2   m: 29
125         ##   n: 3   m: 46
126         ##   n: 4   m: 57
127         ##   n: 5   m: 107
128         ##   n: 6   m: 146
129         ##   n: 7   m: 152
130         ## when run forwards or backwards
131         ## for all 1-bit and 2-bit diffs
132         ## with diffs defined by either xor or subtraction
133         ## with a base of all zeros plus a counter, or plus another bit, or random
134         ##
135         #@staticmethod
136         #void ShortMix(uint64& h0, uint64& h1, uint64& h2, uint64& h3) nogil
137 
138         ##
139         ## Mix all 4 inputs together so that h0, h1 are a hash of them all.
140         ##
141         ## For two inputs differing in just the input bits
142         ## Where "differ" means xor or subtraction
143         ## And the base value is random, or a counting value starting at that bit
144         ## The final result will have each bit of h0, h1 flip
145         ## For every input bit,
146         ## with probability 50 +- .3% (it is probably better than that)
147         ## For every pair of input bits,
148         ## with probability 50 +- .75% (the worst case is approximately that)
149         ##
150         #@staticmethod
151         #void ShortEnd(uint64& h0, uint64& h1, uint64& h2, uint64& h3) nogil

後半が全部コメントアウトになっているのは「疲れた」から。何も全部必要なわけではないし。

cdef cppclass のみ書いた。これはただ C++ のヘッダファイルを写経しているに等しい。SWIG と違い、自動的には何もしてくれない。pxd の役割は cdef cppclassを書く場所、というわけではなく「ヘッダファイルのようなもの」なので、Cython コードに「公開したいもの」を書く。

nogil キーワードには注意して欲しい。これは「GIL から解放される」ために使う(C API を知っている人ならわかる)もので、性能問題を解決しうる「魔法」だが、これは諸刃の剣。説明はしないが、わからない人は付けないほうがいい。ちゃんと理解出来ている人だけが使うこと。

pyx はこうなった:

./lib/jenkins_hash/spooky.pyx
 1 # -*- coding: utf-8 -*-
 2 # This is just for explanation of Cython in http://hhsprings.pinoko.jp/site-hhs/,
 3 # thus, DO NOT USE FOR THE PURPOSE OF OFFICIAL USAGE, AND DO NOT TRUST ME.
 4 cimport spooky as impl
 5 
 6 #
 7 cpdef tuple hash128(bytes s):
 8     cdef impl.uint64 h1
 9     cdef impl.uint64 h2
10     cdef const void* rawval = <const void*>s
11     impl.SpookyHash.Hash128(rawval, len(s), &h1, &h2)
12     return (h1, h2)
13 
14 #
15 cpdef long hash64(bytes s, long seed):
16     cdef const void* rawval = <const void*>s
17     return impl.SpookyHash.Hash64(rawval, len(s), seed)
18 
19 #
20 cpdef int hash32(bytes s, int seed):
21     cdef const void* rawval = <const void*>s
22     return impl.SpookyHash.Hash32(rawval, len(s), seed)
23 
24 #
25 cdef class Spooky(object):
26     cdef impl.SpookyHash* _impl
27 
28     def __cinit__(self):
29         self._impl = new impl.SpookyHash()
30 
31     def __dealloc__(self):
32         del self._impl
33 
34     def __init__(self, long seed1, long seed2):
35         self._impl.Init(seed1, seed2)
36 
37     cpdef update(self, bytes s):
38         self._impl.Update(<const void*>s, len(s))
39 
40     cpdef tuple final(self):
41         cdef impl.uint64 h1
42         cdef impl.uint64 h2
43         self._impl.Final(&h1, &h2)
44         return (h1, h2)

今でこそ私はまぁまぁスラスラ書けるけれど、はじめて Cython に取り組むと、かなり混乱すると思う。少なくとも「PyObject*」の知識なく「本気の Cython」はかなりキビシイ。(挙げた例はだからかなり簡単な部類である。)

ビルドするには、(Visual Studio C++ for Python 2.7 の環境で)

1 me@host ~$ python setup.py build_ext --inplace

とすると良い、最初は。pyxと同じ場所に生成物が作られるのでわかりやすい。

OK。

なお、Windows なので、「.pyd」が作られる。これは「DLL」。UNIX系OSでは「.so」ね。

Cython で作られたものは結局のところ「PythonからはPythonモジュールにしかみえない」。やってみよう。今の場合、lib に cd して python 起動し、

 1 Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32
 2 Type "help", "copyright", "credits" or "license" for more information.
 3 >>> from jenkins_hash import spooky
 4 >>> h = spooky.Spooky(100, 200)
 5 >>> h.update("a" * 1000)
 6 >>> h.update("b" * 2000)
 7 >>> h.update("c" * 3000)
 8 >>> h.final()
 9 (7496187418749914520L, 14141243519903572532L)
10 >>> 

OK。(結果が正しいかどうかは知らないよ。テストしてないんだから。)

Windows でも Cython を使おう

「上のコードについて、メモリアロケーションがどのように行われているのかと「Pythonオブジェクトとの行き来」について想像してください」が何言ってるのかわからん、という人は、やめたほうがいいです。雰囲気だけで出来るシロモノではないから。

それでもなお、実際問題、「直接 Python C API だけで書く」よりは遥かに容易に書けるのは確かだし、「直接 Python C API だけで書くよりもオーバヘッドが高い」と言われることはあるけれどもそれは「安全性」とのバランスである。上に上げたコードが「なんだ簡単じゃねーか」と思えるなら、どんどん Cython 使おう。「速くても安全」なプログラムを「比較的簡単に」書けます。