Edited at

Pythonを速くしたいときにやったこと


前置き

pythonは最近機械学習関連で非常に活躍しているのは周知の事実だと思う

最近話題なのがdeeplearningとか

重たい処理を2万とか3万とかループしたりするので非常に時間がかかったりする

そんなシステムで1回の処理を1秒も減らせれば最終的な恩恵は計り知れないものになる

そういうことを目指して試行錯誤した結果をメモ程度に残しておく

このページではjoblibとかcythonとかは触れません

あくまでライブラリとか関係ないpythonの部分だけ

追記

numbaも試した

https://qiita.com/open_cans/items/b6cde6c28f8eacba0ca1


1.while文はやめておけ!!!

ループをするときはfor文とかwhileとか使うと思う

ひとまず以下のコード実行速度を見て欲しい


whileVSfor.py

import time

start = time.time()
i=0
sumation=0
while(i<1000000):
sumation += i
i+=1
elapsed_time = time.time() - start
print ("while_time:{0}".format(elapsed_time) + "[sec]")

start = time.time()
i=0
sumation=0
for i in range(1000000):
sumation+=i
elapsed_time = time.time() - start
print ("for_time:{0}".format(elapsed_time) + "[sec]")


while_time:0.13159680366516113[sec]

 for_time:0.06858682632446289[sec]

whileは遅い(断言)


2.for文の使い方に注意


2.1 参照の回数に注意

例えば以下のようなリストがあったとする

temp=[1,2,3,4,5,6,7,8,9,10]


1.py

for j in range(100000): 

for i in temp:
sumation += i


2.py

for j in range(100000): 

for i in range(10):
sumation+=temp[i]

1_time:0.06909036636352539[sec]

2_time:0.11903905868530273[sec]

考えれば当然だけど2は毎回参照しているので遅い


2.2 rangeに入れるなら一時変数

例えばこういうfor文


sample.py

for i in range(len(IMAGE)):

for i in range(len(IMAGE[0])):

一時変数を入れた時のものと比較してみよう


temp.py

import time

import numpy

image = numpy.ones((1000,1000,3))
#1
start = time.time()
sumation=0
for i in range(len(image)):
for j in range(len(image[0])):
sumation += i
elapsed_time = time.time() - start
print ("1_time:{0}".format(elapsed_time) + "[sec]")

#2
start = time.time()
sumation=0
lengthY=len(image)
lengthX=len(image[0])
for i in range(lengthY):
for j in range(lengthX):
sumation += i
elapsed_time = time.time() - start
print ("2_time:{0}".format(elapsed_time) + "[sec]")


1_time:0.09444594383239746[sec]

2_time:0.0781855583190918[sec]

改善幅は軽微に見えるかもしれないが1000*1000のループで0.01秒も違うと最終的に結構違うので大事にしてほしい


2.3リストを作るなら内包表記にしよう!

[y,x]のリストを作ることを考えよう


2.3.1 基本的な内包表記

例えばこういうやつ


sample.py

list1=[]

for y in range(1000):
for x in range(1000):
list1.append([y,x])

こいつを内包表記で書くとこうなる


sample.py

list1 = [[y,x] for y in range(1000) for x in range(1000) ]


速度差はこんなもん

2重for文:0.4291651248931885[sec]

内包表記:0.31380796432495117[sec]

基本的に内包表記のほうが早い


2.3.2 if文がはいるとどうなるの?


sample.py

import time

import numpy

image = numpy.ones((1000,1000,3))

start = time.time()
list1=[]
for y in range(1000):
for x in range(1000):
if (y+x)%2==0:
list1.append([y,x])
elapsed_time = time.time() - start
print ("2重for文time:{0}".format(elapsed_time) + "[sec]")

list1=None

start = time.time()
list1 = [[y,x] for y in range(1000) for x in range(1000) if (y+x)%2==0]
elapsed_time = time.time() - start
print ("内包表記time:{0}".format(elapsed_time) + "[sec]")


とりあえずコードを張る

比較対象は上記コードのうちの


sample.py

for y in range(1000):

for x in range(1000):
if (y+x)%2==0:
list1.append([y,x])

上と下


sample.py

list1 = [[y,x] for y in range(1000) for x in range(1000) if (y+x)%2==0]


2重for文time:0.3176920413970947[sec]

内包表記time:0.2414243221282959[sec]

やっぱり内包表記がナンバーワン


3.if文の条件式にも注意

とりあえずこれを見ていただきたい


sample.py

#1

for i in range(100000):
if i%2==0 or i%(3**4**2):
sumation+=i

#2
for i in range(100000):
if i%(3**4**2) and i%2==0:
sumation+=i


1_time:0.018041372299194336[sec]

2_time:0.012027978897094727[sec]

10回ぐらい計測したものの平均値を出している

ここからわかることはif文の条件式の順番が違うだけで速度に差が出るということ

or条件の時は項が複数あっても1つTrueになれば結果は同じなので最初の項でTrueなら後は見ないということかもしれない

ついでにandもやってみた

1_time:0.015639543533325195[sec]

2_time:0.0156252384185791[sec]

まぁ知ってた


3.1 and条件をor条件に変えてみる

例えば not(A and B) みたいな条件式は not(A) or not(B)で置き換えることができる(ド・モルガンの法則)

上記のor条件の仮定が正しければ

not(A and B)

not(A) or not(B)

の二つを判定する際、下の方が早くなるはずである

ということで検証用のコードをペタリ


sample.py

#1

for i in range(10000000):
if not(i%(7**3**2)==0 and i%(3**4**2)==0):
sumation+=i

#2
for i in range(10000000):
if not(i%(7**3**2)==0) or not(i%(3**4**2)==0):
sumation+=i


計測時間は以下の通り

1_time:1.3015859127044678[sec]

2_time:1.3581414222717285[sec]

なんか下の方が重い

もしかしてnotで遅くなっているのではないかと両者のnotの数を1にして計測


sample.py

#1

for i in range(10000000):
if not(i%(7**3**2)==0 and i%(3**4**2)==0):
sumation+=i

#2
for i in range(10000000):
if not(i%(7**3**2)==0) or i%(3**4**2)!=0:
sumation+=i


1_time:1.330047845840454[sec]

2_time:1.32669997215271[sec]

ほんのちょっとだけ2の方が早い

一回あたり0.3ナノ秒ぐらい

2番目にnotが一つも無い状態も計測

1_time:1.3008067607879639[sec]

2_time:1.209193468093872[sec]

どうやらorとかandよりnotの数が計測時間に影響を与えるようだ

ここから想定されることはなるべくnotの数が少なくなるように条件式を変換するのがよさそうだ


4.mapを使おう!!


sample.py

A = '010'

num = int(A)

みたいな感じで変数を変換することがあると思う

こういうときはmapを使うと早くなる


sample.py

import time

import numpy

image = numpy.ones((100000000),dtype=numpy.float32)
sumation=0
length = len(image)
start = time.time()
for i in range(length):
image[i] = int(image[i])
elapsed_time = time.time() - start
print ("1_time:{0}".format(elapsed_time) + "[sec]")

list1=None

start = time.time()
list(map(int,image))
elapsed_time = time.time() - start
print ("2_time:{0}".format(elapsed_time) + "[sec]")


1_time:33.37748670578003[sec]

2_time:0.0[sec]

2_time:11.241594552993774[sec]

mapを使おう!


4.1 numpy.float32の配列にはmapを使うな!!

上記でmapは基本的に早いと説明したがpythonの実装ミスなのかnumpyの実装ミスなのかnumpy.float32にmapを適用しようとすると遅い。

いっそnumpy.float64に変換した方が良いほどだ


np_float32.py

import numpy as np

from time import time

check = np.arange(0.000001, 0.1, 0.000001) #生成されるデータはnumpy.float64型
change = check.astype(np.float32)
print(len(change ), type(change[0]))

start=time()
list(map(lambda x :x-1, change ))
time()-start



np_float64.py

import numpy as np

from time import time

check = np.arange(0.000001, 0.1, 0.000001)
print(len(check), type(check[0]))

start=time()
list(map(lambda x :x-1, check))
time()-start


np.arangeの出力はnumpy.float64型である。この時の実行速度は

numpy.float32 : 0.17655706405639648

numpy.float64 : 0.029918670654296875

となる。以下にnumpy.float32をnumpy.float64に変換してmapをかけ、numpy.float32に変換した場合の処理を示す。

この場合でもnumpy.float32をそのままmapを使うよりは早い


convert.py

import numpy as np

from time import time

check = np.arange(0.000001, 0.1, 0.000001)
change = check.astype(np.float32)

start=time()
convert = change.astype(np.float64)
temp = np.asarray(list(map(lambda x :x-1, convert)))
convert = temp.astype(np.float32)
convert = list(convert)
time()-start


numpy.float32 → map → numpy.float64 → listの順で実行: 0.04291200637817383

もはや意味が分からない。なお基本的な計算はnumpy.float32の方が早い。

pythonのバージョンの問題の可能性もあるので示すがPython 3.6.5でjupyter notebookでの実行結果である


5.tupleを使おう!

例えば逐次処理されるデータをlistにまとめてpandasに変換して最終結果を出力・・・というような処理の場合はtupleを使うと良い。


1.py

import numpy as np

random_list=np.random.rand(10**7)

start=time()
list_test=[ [i,i] for i in random_list]
b=pd.DataFrame(list_test)
print("list:"str(time()-start))



2.py

import numpy as np

random_list=np.random.rand(10**7)

start=time()
tuple_test=((i,i) for i in random_list)
a=pd.DataFrame(tuple_test)
print("tuple:"str(time()-start))


100回平均の実行速度は以下

list:6.02654489517212

tuple:3.84385318040848

4割近くも縮めることができる。配列のサイズによっても変わってくるのでグラフにする。



横軸は配列のサイズ、縦軸は処理時間の100回平均

配列のサイズ
tuple
list

10^5
0.033
0.062

10^6
0.380
0.588

10^7
3.843
6.026

配列の1サイズに対して処理時間がおおよそ決まっており積もり積もって処理時間に大きな差がうまれるようだ。


まとめ

・whileは使わない

・たくさんアクセスするなら一時変数を使おう?

・リスト作るときにfor文使うなら内包表記を使おう

・if文の条件式を見直そう

・mapを使おう

・appendが必要な時にtupleが使えるなら使おう!

if文とかマイクロ秒レベルの話になってしまったけれどきっと誰かの役に立つと信じて