python3

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

前置き

pythonは最近機械学習関連で非常に活躍しているのは周知の事実だと思う
最近話題なのがdeeplearningとか
重たい処理を2万とか3万とかループしたりするので非常に時間がかかったりする
そんなシステムで1回の処理を1秒も減らせれば最終的な恩恵は計り知れないものになる
そういうことを目指して試行錯誤した結果をメモ程度に残しておく

このページではjoblibとかcythonとかは触れません
あくまでライブラリとか関係ないpythonの部分だけ

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_timeが内包表記)
1_time:0.4291651248931885[sec]
2_time: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 ("1_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 ("2_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]

1_time:0.3176920413970947[sec]
2_time:0.2414243221282959[sec]

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

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.1マイクロ秒ぐらい

2番目にnotが一つも無い状態も計測
1_time:1.3008067607879639[sec]
2_time:1.209193468093872[sec]

どうやらorとかandよりnotの数が計測時間に影響を与えるようだ
ここから想定されることはなるべくnotの数が少なくなるように条件式を変換するのがよさそうだ

4.mapを使おう!!

sample.py
A = 'test'
num = int(A)

みたいな感じで変数を変換することがあると思う
こういうときはキャストを使うと早くなる

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()
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]

mapを使おう!

まとめ

・whileは使わない
・たくさんアクセスするなら一時変数を使おう?
・リスト作るときにfor文使うなら内包表記を使おう
・if文の条件式を見直そう
・mapを使おう

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