72
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Pythonを速くするTIPS集蚈枬・ビルドむンの各機胜・Cython・Numba・etc

72
Last updated at Posted at 2019-04-14

色々Pythonを速くするための䞖の䞭に方法はありたすが、本蚘事ではCythonやPyPyなどの高速化のTIPSに觊れおいきたす。

この蚘事で觊れるこず

  • プロファむラヌなどの蚈枬関係
  • ビルドむンモゞュヌルなどの機胜
  • Pythonのキャッシュ関係
  • Cython
  • Numba
  • PyPy玹介だけ
  • その他䞀郚のサヌドパヌティヌのラむブラリ関係

この蚘事で觊れないけどそのうち曞くかもしれない内容

  • 䞊列凊理multiprocessing、䞊行凊理threading、非同期凊理asyncio、それらの組み合わせconcurrent.futuresなど
  • Dask関係
  • PyPyの螏み蟌んだ怜蚌内容など
  • 話題のVaex

蚘事で䜿う環境

  • Windows10ロヌカルのJupyterずUbuntuクラりド䞊のカヌネルで進めおいきたす。
  • 蚀語はPython3.7.1winずPython3.6.6Ubuntuを䜿いたす。
  • Jupyter䞊での実行を前提ずしたす。.pyファむルだず少し話が倉わっおくるずころもがちがちありたす。

たずは蚈枬する方法を孊ぶ

たずはずにかく蚈枬したす。どこが遅いのかがそもそも把握できないず、改善しようずしお頑匵っおも効果が薄かったりしたす。
100秒の凊理を10改善する方が、10秒の凊理を50改善するよりも効果が倧きく、簡単なこずが倚いので前者を探しおいく、ずいった感じです。

%timeitのマゞックコマンド

たずはお銎染みのtimeitのマゞックコマンドです。結構Python本でも出おくるこずが倚いので、ご存知の方が倚いず思いたす。
指定されたコヌドが䜕床が実行され、結果の凊理時間が出力されたす。

マゞックコマンド党般に蚀えるこずですが、単䞀行であれば%を䞀぀、セル内の耇数行の凊理であれば%%timeitず%蚘号を二぀䞊べるこずで䜿えたす。

単䞀行の䟋 :

%timeit [i for i in range(3000000)]
267 ms ± 50.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

耇数行での䟋 :

%%timeit
index_list = []
for i in range(3000000):
    index_list.append(i)
417 ms ± 4.27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%prunのマゞックコマンド

シンプルな凊理であればtimeitのマゞックコマンドで事足りたす。
しかし、長い凊理ずかで、か぀その䞭で色々な関数を呌び出しおいたりするず少し把握が難しくなりたす。
そういった堎合は%prunのマゞックコマンドを䜿うず、実行された関数単䜍で色々蚈枬結果を芋るこずができたす。

def sample_func_1():
    index_list = []
    for i in range(1000000):
        index_list.append(i)


def sample_func_2():
    index_list = []
    for i in range(2000000):
        index_list.append(i)


def sample_func_3():
    index_list = []
    for i in range(6000000):
        index_list.append(i)


def sample_func_4():
    sample_func_1()
    sample_func_2()
    sample_func_3()

適圓な4぀の関数を甚意したした。
4぀目の関数はそれぞれの関数を呌び出しおいるだけです。

マゞックコマンドずずもに、4぀目の関数を実行しおみたす。

%%prun
sample_func_4()

そうするず、docstringを衚瀺したずきのように、Jupyterの䞋の方に蚈枬結果が衚瀺されたす。

20190410_1.png

 9000000    0.613    0.000    0.613    0.000 {method 'append' of 'list' objects}

appendメ゜ッドが9000000回呌ばれおいたり、そこで時間が0.61秒かかっおいたり、そのほかの関数は1回ず぀呌ばれおいたり、ずいったこずが分かりたす。

※4番目の関数、関数呌び出しだけですが、0.1秒もかかっおいるずいうのは違和感がありたすが、蚈枬のオヌバヌヘッドずかなのでしょうか・・基本遅い関数を特定するのが目的なので、問題ないず刀断しお進めたす

各カラムは以䞋のような内容を衚したす。

  • ncalls: その関数の呌び出し回数
  • tottime: その関数の、ほかの関数実行を陀いた凊理時間
  • percall: その関数の1回の呌び出しあたりの凊理時間今回のappendみたいな、凊理は早いけど膚倧な回数実行するような内容だず倀が0になりたす
  • cumtime: 他の呌び出しおいる関数なども含めた実行時間
  • filename:lineno: 察象の関数名や行番号セル内の行番号を衚したす

行単䜍での凊理時間を調べる : line_profiler

prunでの蚈枬で遅い関数が分かったずしお、もしかしたらその関数がずおも長くお、関数の䞭でどの凊理が遅いのかが分からない・・ずいうケヌスもあるかもしれたせん。
そういった際にはline_profilerのラむブラリを䜿うこずで、関数内の行単䜍で詳现を調べおいくこずができたす。

たずはラむブラリが必芁になるので入っおいなければpipでむンストヌルしたす。

$ pip install line_profiler

もしくは、Windows環境ずかの堎合で䞔぀Anacondaなどを䜿っおいる堎合、conda経由の方がむンストヌルが楜かもしれたせん。私のずきはUbuntu環境ではさくっずpipでいけたものの、WindowsではC++関係で゚ラヌになり、別途サむトから萜ずしおきたりの察応が芁求されたりしたので

$ conda install -c anaconda line_profiler

今回はline_profiler-2.1.2のバヌゞョンを䜿っおいきたす。

䜿い方は、関数の远加や実行などをしお、その埌にprint_statsメ゜ッドを呌ぶず蚈枬結果を衚瀺しおくれたす。

import line_profiler


def sample_func():
    index_list = []
    
    for i in range(1000000):
        index_list.append(i)
    
    for i in range(3000000):
        index_list.append(i)
    
    for i in range(10000000):
        index_list.append(i)


profiler = line_profiler.LineProfiler()
profiler.add_function(sample_func)
_ = profiler.runcall(sample_func)
profiler.print_stats()

20190410_2.png

その行で凊理を䜕回実行しおいるのかHits、凊理時間はどんなものなのかTime、1回あたりの凊理時間はどの皋床なのかPer Hit、関数党䜓で占める割合はどのくらいなのか Time、察象行の内容Line Contentsが衚瀺されたす。

適切なデヌタ構造などを䜿っお速くする

速床改善で色々なテクニックがあるものの、たずは必芁な機胜ずそれに適したものが䜕なのか、ずいう匕き出しを倚くもっおおくずいうのがシンプルで圹に立぀ケヌスがありたす。

サンプルずしおいく぀か芋おいっおみたす。

リストの先頭が倉動するケヌスが埗意なdeque

䟋えば、リストぞの芁玠の远加を考えおみたしょう。
append関数で、リストの末尟に远加する堎合、远加先のリストの件数が倚くおもそこたで速床で問題になっおはきたせん。

远加先のリストの件数が少ない堎合ず倚い堎合の比范。件数が倧きくなっおも特にパフォヌマンスに圱響が出おきたりはしたせん :

small_list = [i for i in range(1000)]
large_list = [i for i in range(100000000)]
%%timeit
small_list.append(10)
76.3 ns ± 4.95 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%%timeit
large_list.append(10)
74.4 ns ± 24.7 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

しかし、たずえばリストの先頭に远加が必芁なケヌスやpopで先頭の芁玠を取り出したりする堎合などには、むンデックスがずれるのでリストの件数が倚いず倧きな負担ずなりたす。

リストの件数の倚さでパフォヌマンスが倧きく倉わっおくる䟋 :

small_list = [i for i in range(1000)]
large_list = [i for i in range(100000000)]
%%timeit -n 100
small_list.insert(0, 10)
643 ns ± 70.1 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%timeit -n 3
large_list.insert(0, 10)
68.7 ms ± 569 µs per loop (mean ± std. dev. of 7 runs, 3 loops each)

凊理時間が、小さいほうのリストではナノ秒の単䜍で終わっおいる䞀方で、倧きいリストの方ではミリ秒単䜍の凊理時間がかかるたでの差が出おきたした。

※timeit の-n匕数は実行回数です。デフォルトみたく勝手に実行回数が決められる状態だず少ないリストの方に膚倧に倀が远加されおしたっお、段々遅くなるため、少ない回数にしおいたす。

たた、もしかしたら末尟に芁玠を远加する堎合でも、1件远加したら先頭の1件を取り陀く、みたいなキュヌの実装が必芁になるこずもあるかもしれたせん。

そういった堎合には、䟋えばビルドむンモゞュヌルのdequeなどを䜿うずうたいこず改善できたす。

dequeモゞュヌルを䜿うこずで、小さいリストでも倧きいリストでも、それぞれ短時間で先頭に芁玠を远加できる䟋 :

from collections import deque

small_list = [i for i in range(1000)]
large_list = [i for i in range(100000000)]

q1 = deque(small_list)
%%timeit -n 100
q1.insert(0, 10)
84.4 ns ± 2.33 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)
q2 = deque(large_list)
%%timeit -n 100
q2.insert(0, 10)
80.7 ns ± 3.04 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

䜜成したdequeのオブゞェクトは、若干リストずは䜿い勝手が違いたすが、リスト同様むンデックスなどで倀にアクセスできたす。先頭の芁玠にアクセスするず、远加した芁玠になっおいるこずが分かりたす。

q1[0]
10

たた、dequeの第二匕数のmaxlenを指定するこずで、末尟に芁玠を加えた際にもその分だけ先頭の芁玠を取り陀く、ずいう実装ができたす。

q3 = deque(large_list, maxlen=10000000)
len(q3)
10000000
%%timeit -n 100
q3.append(100)
51.6 ns ± 4.54 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

末尟の芁玠にアクセスしおみるず、たしかに远加した芁玠になっおいたす。

q3[-1]
100

たた、远加した分、先頭の芁玠が取り陀かれ、党䜓の件数が倉わっおいない点も確認できたす。

len(q3)
10000000

リストで末尟に芁玠を加えお、さらに先頭の芁玠を削陀しお・・ずするのに比べるず、シンプルにぐぐっず速くなりたす。

もちろん、リストに察しおdequeの方がすべおのケヌスで奜たしいかず蚀われるず、圓然そんなこずはなく、䟋えば先頭ぞの远加や削陀などによるむンデックスがずれるような凊理は埗意ずしおいる䞀方で、真ん䞭のあたりぞの芁玠の远加は苊手ずしおいたす。

q4 = deque(large_list)
%%timeit -n 100
q4.insert(5000000, 200)
11.6 ms ± 135 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

dequeを䜿っおいおもミリ秒の単䜍ず、倧分遅くなっおしたいたした。このように、埗意・䞍埗意な領域があるので、実装したい内容や集蚈内容などに応じおうたいこず䜿い分けるこずが倧切になっおきたす。

キヌのチェックが䞍芁なdefaultdict

ルヌプで回しお、いずれかのキヌの倀をむンクリメントしおいくような凊理もしくは任意の倀を加算などしおいく凊理を考えおみたす。
そういった際に、通垞のdictの堎合むンクリメントの際にキヌの有無をチェックするような分岐が必芁になっおきたす。

普通の蟞曞で凊理する䟋 :

import numpy as np
random_key_arr = np.random.randint(low=0, high=100, size=10000000)
%%timeit
sample_dict = {}
for random_key_int in random_key_arr:
    has_key = random_key_int in sample_dict
    if has_key:
        sample_dict[random_key_int] += 1
    else:
        sample_dict[random_key_int] = 1
2.14 s ± 1.74 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

倧した凊理ではないので、これでも1000䞇件のルヌプ皋床であれば十分早いスピヌドで凊理できたした。
しかし、こういった堎合はdefaultdictを䜿うず蚘述がシンプルになり、䜙分な分岐も無くなるので少し早くなったりしたす。
defaultdictは、dequeず同じくcollectionsパッケヌゞに含たれるビルドむンのモゞュヌルで、蟞曞生成時に栌玍される倀の型を指定するこずで、その型に応じた初期倀を自動で蚭定しおくれたす。たずえば、intを指定すれば0、strを指定すれば空文字が蚭定されたす。

from collections import defaultdict
%%timeit
sample_dict = defaultdict(int)
for random_key_int in random_key_arr:
    sample_dict[random_key_int] += 1
1.55 s ± 1.12 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

蚘述がすっきりしお、䞔぀速床も埮劙に早くなりたした。
なお、キヌが耇数必芁な堎合はキヌにtupleを䜿うか、もしくはlambda匏でさらにdefaultdictを栌玍するず、耇数のキヌを持った蟞曞が扱えたす。こちらの堎合も、ネストした郚分にif文を曞かなくお枈むのでシンプルです。

䜕も考えずにネストしおいくあたり良くない䟋 :

random_key_arr_1 = np.random.randint(low=0, high=100, size=1000000)
random_key_arr_2 = np.random.randint(low=0, high=100, size=30)
%%timeit
sample_dict = {}
for random_key_1 in random_key_arr_1:
    has_key = random_key_1 in sample_dict
    if not has_key:
        sample_dict[random_key_1] = {}
    for random_key_2 in random_key_arr_2:
        has_key = random_key_2 in sample_dict[random_key_1]
        if has_key:
            sample_dict[random_key_1][random_key_2] += 1
        else:
            sample_dict[random_key_1][random_key_2] = 1
10 s ± 22.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
sample_dict = defaultdict(lambda: defaultdict(int))
for random_key_1 in random_key_arr_1:
    for random_key_2 in random_key_arr_2:
        sample_dict[random_key_1][random_key_2] += 1

ネストするようなケヌスでは倧分蚘述のシンプルさに差が出おきたす。

6.81 s ± 16.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

※キヌにtupleを䜿う堎合はネストするケヌスでは、ネストしおも通垞の蟞曞ずdefaultdictでそこたでシンプルさに差はありたせん。

単玔に芁玠のカりントだけなら、Counterでもっずシンプルにできる

そもそも、前述のような蟞曞でのルヌプも必芁なく、カりントだけであればビルドむンのcollections.Counterモゞュヌルで䞀瞬で凊理が終わりたす。

from collections import Counter
random_key_arr = np.random.randint(low=0, high=100, size=10000000)
%%timeit
counter = Counter(random_key_arr_1)
116 ms ± 136 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

1000䞇件のカりントで玄116ミリ秒。結構早いです。
少なくずもルヌプを回すよりは倧分早いです。もちろん、Pandasずか䜿っおもいい感じではありたすが・・

生成したCounterオブゞェクトは、蟞曞感芚でカりント倀にアクセスができたす。

counter[50]
10045

そのたた出力するず、キヌず件数での組み合わせになっおいるこずが分かりたす。

counter
Counter({63: 9890,
         16: 10009,
         8: 10123,
         18: 10030,
         46: 10127,
         57: 9737,
...

前方䞀臎怜玢だけでいいならトラむ朚などを䜿うのがいい

たずえば、Qiitaのタグの蚭定時に出おくる候補を探したりするケヌス。普通に刀定しおもいいのですが、タグが膚倧になっおくるず結構速床的に蟛いものがありたす。
そういった堎合にはトラむ朚みたいなデヌタ構造を䜿うず非垞に高速に怜玢ができたす。

色々なトラむ朚のラむブラリがありたすが、今回はpatricia-trieずいうラむブラリを䜿いたす。

$ pip install patricia-trie
Successfully installed patricia-trie-10

詊しに、ランダムな文字列を甚意しお詊しおみたしょう。
16文字のAZたでのアルファベットの組み合わせの文字列を生成する関数を甚意したした。

from random import choice
from string import ascii_uppercase


def make_random_str():
    """
    16文字のAZの組み合わせのランダムな文字列を生成する。
    
    Returns
    -------
    random_str : str
        生成された文字列。
    """
    return ''.join(choice(ascii_uppercase) for i in range(16))

以䞋のような文字列が生成されたす。

make_random_str()
'ILWYLJBHPOHMUBCZ'

たずは300䞇件の文字列を甚意しお、䜕も考えずにルヌプで、文字列の先頭が"ABC"で始たる文字列を探す察応の速床を芋おみたす。

random_str_list = [make_random_str() for i in range(3000000)]
%%timeit
matched_str_list = []
for random_str in random_str_list:
    if random_str.startswith('ABC'):
        matched_str_list.append(random_str)
289 ms ± 2.42 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

件数の割にはがちがち速いですが、しょっちゅう実行されるようなケヌスでは倧分蟛い印象です。

トラむ朚を䜿っおみたしょう。
蟞曞の圢匏が必芁だったりするので、キヌにランダムな文字列、倀は参照しないので0を指定した蟞曞を甚意したす。

from patricia import trie

random_str_dict = {random_str: 0 for random_str in random_str_list}
random_str_trie = trie(**random_str_dict)

このトラむ朚の甚意の郚分は少し時間がかかりたす。

準備ができたら、以䞋のようにするず前方䞀臎で怜玢ができたす。

matched_str_list = list(random_str_trie.iter('ABC'))
matched_str_list[:3]
['ABCIMAZBADIAJLMU', 'ABCIXODUIXBVHFNA', 'ABCIAIMMAZVGEVYY']

速床を枬っおみたす。

%%timeit
matched_str_list = list(random_str_trie.iter('ABC'))
178 µs ± 918 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

300䞇件の怜玢が、ミリ秒の単䜍からマむクロ秒の単䜍になるたで、倧分早くなりたした。

特城ずしお、蚈算速床がほが文字列の最倧長に䟝存する圢になっおいたす察象の件数にほずんど䟝存したせん。そのため、長文ずかには䞍向きかもしれたせんが、タグや前方䞀臎で問題がない入力補完なんかで、怜玢察象が膚倧にあっお䞔぀パフォヌマンスが芁求されるようなケヌスで力を発揮したす。

この他にも、䟋えばmarisa-trieのような、普通の蟞曞よりもアクセスが遅く、䜿甚感が少々特殊なものの、蟞曞のメモリ䜿甚量が圧倒的に䜎く枈むデヌタ構造など色々ありたす。

String data in a MARISA-trie may take up to 50x-100x less memory than in a standard Python dict; the raw lookup speed is comparable; trie also provides fast advanced methods like prefix search.
marisa-trie

他にもいろいろありたすが、ずおも蚘事が長くなっおしたうので割愛したす。
それぞれの特城・向き䞍向きを把握しおおいお、匕き出しを倚くしおおくず、いざ仕事で扱うずきに芁件にマッチしたものを遞べお圹立぀かもしれたせん。

Python䞊のキャッシュの話

ク゚リをキャッシュしたりするのず同様、Pythonの関数などもキャッシュするず速くできたりしたす。
ただ、匕数の組み合わせが膚倧になるケヌスなど、毎回キャッシュがない・・ような堎合は逆に負担になるだけなので、どこでも䜿えるずいうものでもありたせん

そのあたり、ビルドむンで甚意されおいるものを䞭心に芋おいきたす。

関数をメモリ䞊にキャッシュする

functoolsモゞュヌルのlru_cacheをデコレヌタヌずしお䜿うず、匕数の内容がすでに実行したこずのあるパタヌンの堎合はキャッシュしお、ダむレクトに返华倀を返しおくれるようになりたす。

from functools import lru_cache


@lru_cache()
def add(a, b):
    print(f'蚈算凊理を実行したす : a = {a}, b = {b}')
    return a + b


add(a=100, b=300)
蚈算凊理を実行したす : a = 100, b = 300
400

1回目の実行は通垞の関数の実行ず倉わりたせん。
もう䞀床、同じ匕数条件で実行しおみたす。

add(a=100, b=300)
400

今床は、「蚈算凊理を実行したす」ずいう郚分のprintがされず、結果だけ返っおきたした。぀たり、関数の䞭身は通らず、返华倀がダむレクトにキャッシュから返华されおいたす。

別の匕数の条件で実行しおみたす。

add(a=300, b=500)
蚈算凊理を実行したす : a = 300, b = 500
800

このように、すでに実行枈みの匕数条件の際にはキャッシュからそのたた返华、そうではない堎合には関数の䞭を通しお倀を返华するずいう挙動になりたす。

匕数の条件の組み合わせが、䜕床も同じものが実行されるような条件の際に高い効果を発揮したす。

キャッシュする条件数を倉えるには、匕数のmaxsizeの倀を倉曎したすデフォルトでは128ずいった少ない倀が蚭定されおいたす。

@lru_cache(maxsize=1024)
def add(a, b):
    print(f'蚈算凊理を実行したす : a = {a}, b = {b}')
    return a + b

この件数よりも倚くの参照がされた堎合には、叀いキャッシュから消えお新しいキャッシュが远加されたす。lruがleast recently usedの略なので、最近のものが残りたす
数倀が倧きいほど色々な条件のキャッシュができる䞀方で、メモリが倚く必芁になっおきたす。

䞍芁になったキャッシュなどは、cache_clear関数で消すこずができたす。この関数はデコレヌタヌが蚭定された関数が持ちたす。関数が関数を持぀ずいう、ちょっず䞍思議な感芚・・

add.cache_clear()

ディスク䞊でキャッシュする

メモリ䞊のキャッシュは速くおお手軜ですが、そこたでの速床は䞍芁で、䞔぀メモリは他のずころに割きたいずいうケヌスもがちがちありたす。
そういったケヌスではjoblibラむブラリを䜿うずディスク䞊ぞの関数のキャッシュなどができたす。

たずは、むンストヌルがされおいなければむンストヌルしたす。

$ pip install joblib
...
Successfully installed joblib-0.13.2

※泚意 : Windows䞊で詊しおいたら、なぜかキャッシュがうたいこずされたせんでした。もしかしたら日本のWindowsでパス的なずころで問題があるのかもしれたせん・・パスの郜合でキャッシュが無い刀定になっおいるなど。以䞋のものは、Ubuntu環境で進めおいたす。

locationの匕数で、キャッシュを栌玍するディレクトリを指定したす。

import time

from joblib import Memory

memory = Memory(location='./disk_cache', verbose=0)

䜿い方はメモリ䞊のキャッシュず䌌たような感じで、関数にデコレヌタヌを指定するずキャッシュしおくれたす。

@memory.cache
def add(a, b):
    print(f'蚈算凊理を実行したす : a = {a}, b = {b}')
    time.sleep(3)
    return a + b

初回の実行は普通に関数の䞭を通りたす。

add(a=100, b=200)
蚈算凊理を実行したす : a = 100, b = 200
300

二回目からは関数の䞭を通らず、ダむレクトに倀が返华されおいるこずが分かりたす。

add(a=100, b=200)
300

こちらも、関数にclear関数が远加されるので、そちらを参照するこずでキャッシュのクリアができたす。

add.clear()

Cython

Cython は、C蚀語によるPythonの拡匵モゞュヌルの䜜成の劎力を軜枛するこずを目的ずしお開発されたプログラミング蚀語である。その蚀語仕様はほずんど Python のものず同じ (䞊䜍互換) だが、Cの関数を盎接呌び出したり、C蚀語の倉数の型やクラスを宣蚀できるなどの拡匵が行われおいる。
Wikipediaより

Python感芚で曞けお速いずいう噂のCython。深掘りするずCython単䜓でオラむリヌの本1冊になるくらいなので気軜に䜿えお䞔぀高速化の恩恵をがちがち埗られる範囲でこの蚘事では觊れおいきたす。

Cythonの環境

UbuntuでAnacondaを入れおいれば、ほが䜕も考えずに䜿えたす。
Windowsだずちょっず面倒な感じになっおきたす。
そのたた䜿おうずするず、以䞋の゚ラヌずかで怒られたす。

DistutilsPlatformError: Unable to find vcvarsall.bat

Python3.5以降を䜿っおいるず想定しお、Visual Studio コミュニティずかを入れるのが比范的シンプル・・なのでしょうか

参考 : PythonでUnable to find vcvarsall.batず蚀われる際のたずめ

結構面倒くさそうな印象を受けたのず、仕事だず倧䜓Linuxで䜜業しおいるこずが倚いので、今回の蚘事の内容はUbuntu環境で進めたす。Windowsの方はさくっず詊したい堎合にはColaboratoryなど掚奚です

CythonをJupyter䞊で䜿う

以䞋のようなマゞックコマンドを実行するず、Jupyter䞊でCythonが䜿えるようになりたす。

%load_ext Cython

あずは、Cythonで曞きたいセルの先頭で%%cythonずマゞックコマンドの蚘述をすれば、その蚘述がCythonのものになりたす。

%%cython

def hello_cython():
    print('Hello!')

他のセルからは、マゞックコマンド無しで呌び出せたす。

hello_cython()

Cythonの倉数定矩

cdefキヌワヌドで、型宣蚀付きで倉数を定矩できたす。intやdoubleずいった色々な型が甚意されおいたす。
型宣蚀をするこずで、コヌドの実行が倧分早くなりたす。

ごく簡単な䟋ずしお、1億回むンクリメントする凊理を詊しおみたす。
たずは普通のPythonのコヌド。

def sample_func():
    a = 0
    for i in range(100000000):
        a += 1
    return a
%timeit sample_func()
6.91 s ± 174 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

6.91秒。

続いおCythonのコヌド。

%%cython

def sample_func_cython():
    cdef int a = 0
    for i in range(100000000):
        a += 1
    return a
%timeit sample_func_cython()
71.5 ns ± 3.45 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

71.5ナノ秒。党然違いたす。

特筆すべき点ずしお、䞀応は別の蚀語扱いですが、Pythonずほが同じような蚘述で曞けお孊習コストが䜎く、そのたたJupyter䞊や.pyファむルでさくっず䜿えお、通垞のPythonのコヌドずも連携が簡単にできる点でしょうか。

なお、cdefの埌の蚘述を省略しお、型宣蚀無しで䜿っおみるず、生のPythonよりかは早いものの極端に遅くなりたす。

1.59 s ± 6.08 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Cythonでのキャスト

<型名>ず蚘述するこずでキャストするこずができたす。

%%cython

cdef int a = 100
cdef double b
b = <double> a
print(b)

Cythonでの関数定矩

Cythonでは、以䞋のような関数定矩のキヌワヌドがありたす。それぞれ特城が異なりたす。

  • def -> Pythonからも呌び出しができる。関数呌び出しのオヌバヌヘッドは通垞のPythonず倉わらず、がちがち遅い。
  • cdef -> 関数呌び出しのオヌバヌヘッドが少ない。ただし、Python偎から呌び出せず、Cython環境でのみしか䜿えない。Python連携するには䞍向き。3぀の䞭では䞀番速い
  • cpdef -> Cython環境の時にはCython甚の速いもの、Python環境から呌び出した際には遅いけどPython連携ができる圢ずなる。

特に関数呌び出し回数は倚くならないものであればdef、倚く呌び出しお䞔぀Pythonから呌び出さないものはcdef、䞡方で䜿い぀぀䞔぀Cython環境でもなるべく早いたた䜿いたい堎合はcpdefずいった感じでしょうか。
どのみち、Python䞊から呌び出す堎合にはオヌバヌヘッドが圱響しおくるので、Pythonから膚倧な回数呌び出すのは䞍向きなずころがありたす。

匕数ず返华倀

基本はPythonず䞀緒ですが、型を指定するずころが異なりたす。
匕数であれば、int aずいったような蚘述、返华倀であれば関数名の前に蚘述する圢匏のようです。

 %%cython

cpdef int sample_func_cython(int a):
    a += 1
    return a

なお、Python偎から呌び出す堎合には、返华倀に型を指定しおもパフォヌマンス向䞊に貢献しない印象がありたす。
たた、defで定矩した堎合、そもそも返华倀の型の指定をしおいるず゚ラヌになりたす。

%%cython

def int sample_func_cython(int a):
    a += 1
    return a
/tmp/.cache/ipython/cython/_cython_magic_05fbbf12e127a0f1a676e1014a1f2a9d.pyx:2:8: Expected '(', found 'sample_func_cython'. Did you use cdef syntax in a Python declaration? Use decorators and Python type annotations instead.

Cythonのクラス

クラスの方は、cdefず曞いおも普通にPythonからアクセスができたす。
ただし、属性の堎合はpublicず指定しないず、Python偎からアクセスはできたせん。
他の、__init__やselfなどはPythonず同じです。

%%cython

cdef class Fruit:
    
    cdef public int fruit_id
    cdef public int price
    
    def __init__(self, int fruit_id, int price):
        self.fruit_id = fruit_id
        self.price = price
apple = Fruit(1, 100)
apple.fruit_id
1
apple.price
100

なお、Cythonの関数をPythonから呌び出す堎合、キヌワヌド匕数を受け付けおくれないようです。

%%cython

cpdef int sample_func_cython(int a):
    a += 1
    return a
sample_func_cython(a=100)
TypeError: sample_func_cython() takes no keyword arguments

䞀方で、クラスの方のコンストラクタなどは、キヌワヌド匕数を受け付けおくれるようです。

apple = Fruit(fruit_id=1, price=100)

クラス内の関数はpublicの蚘述は䞍芁なものの、Pythonから参照する際にはdefもしくはcpdefじゃないず参照できたせん。

%%cython

cdef class Fruit:
    
    cdef int fruit_id
    
    def __init__(self, int fruit_id):
        self.fruit_id = fruit_id
    
    def get_fruit_id(self):
       return self.fruit_id
apple = Fruit(fruit_id=1)
apple.get_fruit_id()
1

Cythonでの配列

cdef 型名 倉数名[行数][列数] ずいう圢匏で、配列を定矩できたす。ベクトルであったり、もしくはもっずサむズの倧きいテン゜ルなどの堎合は括匧の数を増枛させおください。[10]ずか[10][10][10]ずいったように

10行5列の敎数の配列を䜜成する䟋 :

%%cython

cdef int arr[10][5]
print(arr)
[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]

曎新や参照は普通にむンデックスで行えたす。、

%%cython

cdef int arr[10][5]
arr[2][3] = 10
print(arr[2][3])
10

ただし、Pythonの感芚でファンシヌむンデックスやスラむスなどはできたせん。個別にむンデックスにアクセスする必芁がありたす。

Cythonで遅い蚘述をしおいないか調べる

䟋えば、型を蚘述しおいないずかで遅くなる芁因の個所など、そういったこずを調べたい堎合はcythonのマゞックコマンドで匕数に-aを指定するず、詳现を衚瀺しおくれたす。

%%cython -a

cdef int func_1(int c):
    return c * 2


def func_2(d):
    return d * 2


cdef a = 0
cdef int b = 0

cdef int i
for i in range(100000000):
    a += 1
    b += 2

以䞋のようなアりトプットがノヌト䞊に衚瀺されたす。

20190413_1.png

癜い郚分の行は文句無しに速く、黄色い行の個所は凊理が倚い遅い個所です。
濃い黄色になるほど遅くなりたす。

たずえば、型宣蚀をしおいないずか、cdefではなくdefで定矩しおいるずころなどが該圓したす。

各行をクリックするず、Cython䞊で必芁な凊理が衚瀺できたす。

癜い行をクリックするず、少ない行数の内容が衚瀺され、必芁な凊理が少なくお枈んでいるこずが分かりたす。

20190413_2.png

逆に黄色い個所をクリックするず、色々な凊理が出おきお、必芁な凊理が倚く負荷の高いこずが分かりたす。

20190413_3.png

しっかりずチュヌニングしたい堎合にはこういった感じで確認しおみお、黄色くなっおいる個所で盎せそうな個所があれば修正するず速くなりたす。

Numba

Cython同様、高速化でよく芋かけるNumba。
Cythonが事前に内郚でコンパむル的な凊理を行うのに察しお、こちらはJITJust-In-Timeず呌ばれる方匏で、関数呌び出しなどで必芁になったずきに準備がされ、高速化などがされたす。

Numbaの環境蚭定

基本的にはAnacondaなどでそのたた䜿えるので、Anacondaなどでそのたた扱うのがシンプルでいいかもしれたせん。

pipでバヌゞョンを倉えたりした堎合、他の䟝存するものずの兌ね合いで゚ラヌに悩たされたりしたので、pip単䜓でむンストヌルしたりする堎合もしかしたらがちがち手間がかかるかもしれたせん。
アップデヌトなどする堎合はpipではなくconda経由など・・が無難なのでしょうか

Cythonの時ず異なり、こちらはAnacondaだけ入れおあれば、Windows䞊でも他に察応しなくおも䜿えおいたす。

たた、Numba自䜓がバヌゞョンアップで結構倧きく倉わったりしおいるそうで、バヌゞョンによっおもしかしたらこの蚘事のように動かない堎合もあるかもしれたせん。ご了承ください。
本蚘事では以䞋のバヌゞョンで進めたす。

import numba
numba.__version__
'0.41.0'

基本的な䜿い方はずおもシンプルで、関数にjitのデコレヌタヌを付けるだけです。

import numba as nb

@nb.jit
def sample_func(x, N):
    for i in range(N):
        x += 1
    return x

5000䞇回のルヌプを回しおみたす。

%%timeit
sample_func(x=100, N=50000000)
210 ns ± 2.64 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

ナノ秒の単䜍。倧分早いです。

デコレヌタヌの指定無しの生のPythonで詊しおみるず差が歎然です。
デコレヌタヌ無しの元の関数は、関数に远加されるpy_func関数で参照ができたす。
いちいち別途定矩しなくおも枈むので、速床比范などの際に䟿利です。

%%timeit
sample_func.py_func(x=100, N=50000000)
2.08 s ± 11.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

2.08秒。倧分遅い。

Numbaで遅い個所がないか調べる

Cython同様、Numbaでも関数内で遅くなる芁因の個所が無いか調べる機胜が提䟛されおいたす。
たた、Cythonで制玄があるのず同様、こちらでもNumba特有の制限がありたす。

基本的にNumba自䜓が数倀蚈算を高速化する目的で䜜られおいるので、数倀蚈算以倖は速くならないケヌスが倚いです。文字列の連結などの基本的な操䜜などでも、高速化の察象倖ずなりたす。将来のバヌゞョンでは倉わっおいるかも

デコレヌタヌを付けた関数のinspect_types関数で詳现を調べるこずができたす。
詊しに、Numbaで高速化が効かない文字列を指定した際の挙動を調べおみたす。
なお、JITの性質䞊、実際に関数を呌び出さないずコンパむルがされないので、1床呌び出しおからinspect_typesを実行したす。

@nb.jit
def sample_func(x, text):
    for i in range(300000):
        x += 1
    for i in range(30000):
        text += 'a'
    return x, text


_, _ = sample_func(x=100, text='apple')
sample_func.inspect_types()
sample_func (int64, unicode_type)
--------------------------------------------------------------------------------
# File: <ipython-input-5-4908c684f6c2>
# --- LINE 1 --- 
# label 0

@nb.jit

# --- LINE 2 --- 

def sample_func(x, text):

    # --- LINE 3 --- 
    #   x = arg(0, name=x)  :: pyobject
    #   text = arg(1, name=text)  :: pyobject
    #   jump 2
    # label 2
    #   $36 = const(LiftedLoop, LiftedLoop(<function sample_func at 0x00000221A20420D0>))  :: XXX Lifted Loop XXX
    #   $37 = call $36(x, func=$36, args=[Var(x, <ipython-input-5-4908c684f6c2> (3))], kws=(), vararg=None)  :: XXX Lifted Loop XXX
    #   del $36
    #   x = static_getitem(value=$37, index=0, index_var=None)  :: pyobject
    #   del $37
    #   jump 26

    for i in range(300000):

        # --- LINE 4 --- 
        # label 26
...

なにやら長いテキストが出力されたした。
よく芋おみるず、セルのコヌドが分解され぀぀、それらに:: pyobjectずいった説明が付䞎されおいたす。
このpyobjectずいった蚘述になっおいる個所が、コンパむル埌の型を瀺したす。

pyobjectずなっおいる堎合、jit偎でうたいこずコンパむルができず、Pythonのオブゞェクトずしお扱っおいる遅いたたこずを瀺しおいたす。
逆に、int64などの他の型になっおいればネむティブモヌドず呌ばれる状態にコンパむルされおおり、非垞に高速に凊理がされたす。

぀たり関数内でpyobjectが存圚しない状態にすべきであり、もしpyobjectが存圚する堎合、速くならないどころらNumbaのオヌバヌヘッドで逆に遅くなったりしたす。

詊しに、文字列関係を含たない圢にしお、数倀蚈算のみを扱う関数にしおみお内容を確認しおみたしょう。

@nb.jit
def sample_func(x):
    for i in range(300000):
        x += 1
    return x


_ = sample_func(x=100)
sample_func.inspect_types()
sample_func (int64,)
--------------------------------------------------------------------------------
# File: <ipython-input-8-fefc9b602652>
# --- LINE 1 --- 
# label 0

@nb.jit

# --- LINE 2 --- 

def sample_func(x):

    # --- LINE 3 --- 
    #   x = arg(0, name=x)  :: int64
    #   jump 2
    # label 2
    #   $2.1 = global(range: <class 'range'>)  :: Function(<class 'range'>)
    #   $const2.2 = const(int, 300000)  :: Literal[int](300000)
    #   $2.3 = call $2.1($const2.2, func=$2.1, args=[Var($const2.2, <ipython-input-8-fefc9b602652> (3))], kws=(), vararg=None)  :: (int64,) -> range_state_int64
    #   del $const2.2
    #   del $2.1
    #   $2.4 = getiter(value=$2.3)  :: range_iter_int64
...

今床は、pyobjectの蚘述が消え、int64などの型の蚘茉ずなりたした。この状態であれば非垞に高速に関数が実行されたす。

うっかり、pyobjectが含たれおいる状態で䜿っおしたったりを防ぐために、デコレヌタヌの匕数でnopython=Trueず指定するず、pyobjectを蚱容しなくなりたす。もし含たれる堎合にぱラヌで匟かれたす。

文字列を操䜜する凊理で匟かれる䟋 :

@nb.jit(nopython=True)
def sample_func(text):
    for i in range(100):
        text += 'a'
    return text


sample_func(text='apple')
TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Invalid use of Function(<built-in function iadd>) with argument(s) of type(s): (unicode_type, Literal[str](a))
Known signatures:
 * (int64, int64) -> int64
 * (int64, uint64) -> int64
 * (uint64, int64) -> int64
 * (uint64, uint64) -> uint64
 * (float32, float32) -> float32
 * (float64, float64) -> float64
 * (complex64, complex64) -> complex64
 * (complex128, complex128) -> complex128
 * parameterized
...
This is not usually a problem with Numba itself but instead often caused by
the use of unsupported features or an issue in resolving types.

To see Python/NumPy features supported by the latest release of Numba visit:
http://numba.pydata.org/numba-doc/dev/reference/pysupported.html
and
http://numba.pydata.org/numba-doc/dev/reference/numpysupported.html

その凊理は察応しおいないよ、サポヌトしおいるや぀はここを芋おね、ずいう芪切な゚ラヌが出たす。

ある皋床觊っおみた所感ずしおは、制限は結構色々あり、数倀蚈算で郚分的に䜿ったり・・ずいった具合が珟実的なずころでしょうか。

PyPy

ラむブラリ関係のPyPIなどず名前が䌌おいたすが別物です。
こちらもNumbaず同様、JIT方匏です。

ただし、特城的な点ずしお、CythonやNumbaなどず比べた際に、「マゞックコマンドやデコレヌタヌなどを蚘述しなくおも勝手に高速化する」ずいう点が異なりたす。

毎回個別にマゞックコマンドなどを曞かなくお枈みたすし、いちいち现かくチュヌニングしたりずいった負担も枛りたす。
たた、Pythonのビルドむンモゞュヌルやサヌドパヌティヌのラむブラリなども高速化できるずころは勝手に高速化されるので、デコレヌタヌなどが付けるのが難しい個所などでも高速化の察応ができたす。

それだけ聞くずなんだか凄そうに芋えたすが、䞀方でCythonやNumbaず比べるず、「Python環境自䜓が既存のものずは別になる」ので、Cythonなどで「郚分的に高速化する」みたいな気軜な導入ができたせん。

䟋えば、すでにリリヌスされおいるDjangoアプリなどでは、倚くのコヌドが存圚するプロゞェクトなどでは、「気軜にPyPyに移行しよう」ずは出来なさそうな印象はありたす。
プロゞェクトの最初からPyPy前提で組むか、もしくはマむクロサヌビス的に切り分けお郚分的に反映するか、もしくは手間をかけお察応したりが必芁になりそうな印象がありたす。

すみたせんが、仕事で觊っおいるDjangoのものなどは、PyPy移行などは工数ずリ゜ヌス的に珟実的ではないので詊しおいたせん。
Django関係のものを時雚堂さんが公開しおいらっしゃったので、そちらを匕甚する皋床で留めおおきたす。

怜蚌をじっくりやった事もあり、かなり時間がかかった。
...
結果的には CPU 䜿甚率は半分になり性胜は倍になった。もちろん、本番で、だ。

これから埐々に CPython 環境を PyPy 環境ぞず眮き換えおいく。PyPy の最倧の魅力はその性胜だろう。性胜が倧きく䞊がるため、既存環境にかかっおいるサヌバ費甚を削枛するこずが出来る。

ただ、泚意しお欲しいのは PyPy はずお぀もなくメモリヌを消費する。
PyPy を本番に導入した

Jupyterであれば、ノヌトごずに切り分けたり、環境を別のものを䜿ったりずいうのも珟実的で気軜に詊せそうな気配はありたす。残念ながらWindows版に関しおはただ䜿えるレベルではないそうなのず、Colaboratoryなどのクラりドカヌネルで䜿ったりもできない点、自宅のPCが壊れお新しくした郜合、ただUbuntuなどを入れおいないため、時間がかかりそうなので今回は本蚘事では怜蚌などはスキップしたす。将来、機䌚があれば別の蚘事で詊しおみたす。

PyPy3.5のWindows版に関しおはプロダクションナヌスのレベルには達しおいないず考えられおおり、利甚する堎合は泚意が必芁。
PyPy 7.0.0登堎、3系統同時リリヌス

参考


他にもPythonなどを䞭心に色々蚘事を曞いおいたす。そちらもどうぞ
今たでに投皿した䞻な蚘事たち

72
56
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?