LoginSignup
515
393

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-08-08

前置き

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

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

追記
numbaも試した
https://qiita.com/open_cans/items/b6cde6c28f8eacba0ca1

2020/10/17追記
初稿から3年経過しpythonのバージョンもだいぶ上がり、記事の情報も古くなってしまったのでpython3.9.0時点での情報に更新する。

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

ループをするときはfor文とかwhileとか使うと思う
ひとまず以下のコード実行速度を見て欲しい

while.py
from statistics import mean,stdev
from time import time

def while_loop():
    start = time()
    i=0
    sumation=0
    while(i<10**6):
        sumation += i
        i+=1
    elapsed_time = time() - start
    return elapsed_time

while_times = [while_loop() for i in range(10**3)]

for.py
from statistics import mean,stdev
from time import time

def for_loop():
    start = time()
    i=0
    sumation=0
    for i in range(10**6):
        sumation+=i
    elapsed_time = time() - start
    return elapsed_time

for_times = [for_loop() for i in range(10**3)]
mean_for = mean(for_times)
stdev_for = stdev(for_times)
print(f"forの平均値:{mean_for}")
print(f"forの標準偏差:{stdev_for}")
hist.py
import pandas as pd
#処理結果をヒストグラムで表示
df = pd.DataFrame(zip(while_times, for_times), columns=["while", "for"])
df["while"].hist(bins=100,color="red")
df["for"].hist(bins=100,color="blue")

for、whileで100万回のループを行う関数を1000回行った

平均値 標準偏差
while 0.069 0.0168
for 0.044 0.0097

ヒストグラム(青:for 赤:while)
image.png

forもwhileもヒストグラムを見ると時間が延びる方向でブレが発生しているようだ。基本的にはforを使っておけば間違いなさそうである。
whileは遅い(断言)

2.for文の使い方に注意

2.1 参照の仕方に注意

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

インデックス指定して参照.py
from statistics import mean,stdev
from time import time

def index_ref():
    sumation=0
    temp=[1,2,3,4,5,6,7,8,9,10]
    start = time()
    for j in range(10**5): 
        for i in range(10):
            sumation+=temp[i]
    return time() - start
index_ref_times = [index_ref() for i in range(10**3)]
mean_index_ref = mean(index_ref_times)
stdev_index_ref = stdev(index_ref_times)
print(f"index_refの平均値:{mean_index_ref}")
print(f"index_refの標準偏差:{stdev_index_ref}")
リストから参照.py
from statistics import mean,stdev
from time import time

def list_ref():
    sumation=0
    temp=[1,2,3,4,5,6,7,8,9,10]
    start = time()
    for j in range(10**5): 
        for i in temp:
            sumation += i
    return time() - start

list_ref_times = [list_ref() for i in range(10**3)]
mean_list_ref = mean(list_ref_times)
stdev_list_ref = stdev(list_ref_times)
print(f"list_refの平均値:{mean_list_ref}")
print(f"list_refの標準偏差:{stdev_list_ref}")
hist.py
import pandas as pd
df = pd.DataFrame(zip(list_ref_times, index_ref_times), columns=["list_ref", "index_ref"])
df["index_ref"].hist(bins=100,color="red")
df["list_ref"].hist(bins=100,color="blue")
平均値 標準偏差
インデックス指定して参照 0.077 0.0076
リストから参照 0.041 0.0082

ヒストグラム(青:リストから参照 赤:インデックス指定して参)
image.png

考えれば当然だけどインデックスで指定する方は毎回場所指定で参照しているので遅い

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

例えばこういうfor文

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

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

毎回lenで計算する.py
from statistics import mean,stdev
from time import time
import numpy as np
image = np.ones((1000,1000,3))

def everytime_calc_len():
    start = time()
    sumation=0
    for i in range(len(image)): 
        for j in range(len(image[0])):
            sumation += i
    return time() - start

every_times = [everytime_calc_len() for i in range(10**3)]
mean_every = mean(every_times)
stdev_every = stdev(every_times)
print(f"毎回lenで計算する時の平均値:{mean_every}")
print(f"毎回lenで計算する時の標準偏差:{stdev_every}")
1回だけ.py
from statistics import mean,stdev
from time import time
import numpy as np
image = np.ones((1000,1000,3))

def onetime_calc_len():
    start = time()
    sumation=0
    for i in range(len(image)): 
        for j in range(len(image[0])):
            sumation += i
    return time() - start

one_times = [onetime_calc_len() for i in range(10**3)]
mean_one = mean(one_times)
stdev_one = stdev(one_times)
print(f"一時変数で置き換えた時の平均値:{mean_one}")
print(f"一時変数で置き換えた時の標準偏差:{stdev_one}")
hist.py
import pandas as pd
df = pd.DataFrame(zip(every_times, one_times), columns=["every", "one"])
df["every"].hist(bins=100,color="red")
df["one"].hist(bins=100,color="blue")
平均値 標準偏差
毎回lenで計算 0.044 0.0074
一時変数に置き換える 0.044 0.0069

ヒストグラム(青:一時変数 赤:毎回lenで計算)
image.png

len程度だと誤差なのでお好きな方をお使いください
改善幅は軽微に見えるかもしれないが1000*1000のループで0.01秒も違うと最終的に結構違うので大事にしてほしい

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

[y,x]のリストを作ることを考えよう。この時for文と内包表記で作成して速度差を計測する。

2.3.1 基本的な内包表記

2重for文.py
from statistics import mean,stdev
from time import time

def for_loop():
    start = time()
    list1=[]
    for y in range(1000):
        for x in range(1000):
            list1.append([y,x])
    return time() - start

for_loop_times = [for_loop() for i in range(10**3)]
mean_for_loop = mean(for_loop_times)
stdev_for_loop = stdev(for_loop_times)
print(f"二重for文の平均値:{mean_for_loop}")
print(f"二重for文の標準偏差:{stdev_for_loop}")
内包表記.py
from statistics import mean,stdev
from time import time

def naihou():
    start = time()
    list1 = [[y,x] for y in range(1000) for x in range(1000) ]
    return time() - start

naihou_times = [naihou() for i in range(10**3)]
mean_naihou = mean(naihou_times)
stdev_naihou = stdev(naihou_times)
print(f"内包表記の平均値:{mean_naihou}")
print(f"内包表記の標準偏差:{stdev_naihou}")
hist.py
import pandas as pd
df = pd.DataFrame(zip(for_loop_times, naihou_times), columns=["for", "naihou"])
df["for"].hist(bins=100,color="red")
df["naihou"].hist(bins=100, color="blue")
平均値 標準偏差
2重for文 0.387 0.024
内包表記 0.337 0.0201

ヒストグラム(青:内包表記 赤:2重for文)
image.png

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

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

2重for文.py
from statistics import mean,stdev
from time import time

def for_loop():
    start = time()
    list1=[]
    for y in range(1000):
        for x in range(1000):
            if (y+x)%2==0:
                list1.append([y,x])
    return time() - start

for_loop_times = [for_loop() for i in range(10**3)]
mean_for_loop = mean(for_loop_times)
stdev_for_loop = stdev(for_loop_times)
print(f"二重for文の平均値:{mean_for_loop}")
print(f"二重for文の標準偏差:{stdev_for_loop}")
内包表記.py
from statistics import mean,stdev
from time import time

def naihou():
    start = time()
    list1 = [[y,x] for y in range(1000) for x in range(1000) if (y+x)%2==0]
    return time() - start

naihou_times = [naihou() for i in range(10**3)]
mean_naihou = mean(naihou_times)
stdev_naihou = stdev(naihou_times)
print(f"内包表記の平均値:{mean_naihou}")
print(f"内包表記の標準偏差:{stdev_naihou}")
hist.py
import pandas as pd
df = pd.DataFrame(zip(for_loop_times, naihou_times), columns=["for", "naihou"])
df["for"].hist(bins=100,color="red")
df["naihou"].hist(bins=100, color="blue")
平均値 標準偏差
2重for文 0.387 0.024
内包表記 0.337 0.0201

ヒストグラム(青:内包表記 赤:2重for文)
image.png

for文の中にif文が入ると処理速度がトントンぐらいになるようだ
お好きな方をご使用ください。

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

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

処理重い項が先.py
from statistics import mean,stdev
from time import time

def if_and1():
    start = time()
    sumation=0
    for i in range(2*10**3):
        if i%(i**i)==0 and i%2==0:
            sumation+=i
    return time() - start

if_and1_times = [if_and1() for i in range(10**3)]
mean_if_and1 = mean(if_and1_times)
stdev_if_and1 = stdev(if_and1_times)
print(f"処理重い項が先の平均値:{mean_if_and1}")
print(f"処理重い項が先の標準偏差:{stdev_if_and1}")

処理重い項が後.py
from statistics import mean,stdev
from time import time

def if_and2():
    start = time()
    sumation=0
    for i in range(2*10**3):
        if i%2==0 and i%(i**i)==0:
            sumation+=i
    return time() - start

if_and2_times = [if_and2() for i in range(10**3)]
mean_if_and2 = mean(if_and2_times)
stdev_if_and2 = stdev(if_and2_times)
print(f"処理重い項が後の平均値:{mean_if_and2}")
print(f"処理重い項が後の標準偏差:{stdev_if_and2}")
hist.py
import pandas as pd
df = pd.DataFrame(zip(if_and1_times, if_and2_times), columns=["and1", "and2"])
df["and1"].hist(bins=100,color="red")
df["and2"].hist(bins=100, color="blue")
平均値 標準偏差
処理重い項が先 0.046 0.0034
処理重い項が後 0.023 0.0029

ヒストグラム(青:処理重い項が後 赤:処理重い項が先)
image.png

ここからわかることはif文の条件式の順番が違うだけで速度に差が出るということ
and条件の時は項が複数あっても1つFalseになれば結果は同じなので最初の項でFalseなら後は見ないということかもしれない

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

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

not(A and B)
not(A) or not(B)
どちらが速くなるのだろうか

notが1つ.py
from statistics import mean,stdev
from time import time

def if_and1():
    start = time()
    sumation=0
    for i in range(3*10**3):
        if not(i%(500**i)==0 and i%(600**i)==0):
            sumation+=i
    return time() - start

if_and1_times = [if_and1() for i in range(10**3)]
mean_if_and1 = mean(if_and1_times)
stdev_if_and1 = stdev(if_and1_times)
print(f"and1の平均値:{mean_if_and1}")
print(f"and1の標準偏差:{stdev_if_and1}")
notが2つ.py
from statistics import mean,stdev
from time import time

def if_and2():
    start = time()
    sumation=0
    for i in range(3*10**3):
        if not(i%(500**i)==0) or not(i%(600**i)==0):
            sumation+=i
    return time() - start

if_and2_times = [if_and2() for i in range(10**3)]
mean_if_and2 = mean(if_and2_times)
stdev_if_and2 = stdev(if_and2_times)
print(f"and2の平均値:{mean_if_and2}")
print(f"and2の標準偏差:{stdev_if_and2}")
平均値 標準偏差
notが1つ 0.104 0.0028
notが2つ 0.104 0.0033

ヒストグラム(青:notが2つ 赤:notが1つ)
image.png

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

誤差

4.mapを使おう!!

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

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

for_cast.py
from statistics import mean,stdev
from time import time

def for_cast():
    image = [1.1 for i in range(10**5)]
    sumation=0
    length = len(image)
    start = time()
    for i in range(length):
        image[i] = int(image[i])
    return time() - start

for_cast_times = [for_cast() for i in range(10**3)]
mean_for_cast = mean(for_cast_times)
stdev_for_cast = stdev(for_cast_times)
print(f"for_castの平均値:{mean_for_cast}")
print(f"for_castの標準偏差:{stdev_for_cast}")
map_cast.py
from statistics import mean,stdev
from time import time

def map_cast():
    image = [1.1 for i in range(10**5)]
    start = time()
    image = list(map(int,image))
    return time() - start

map_cast_times = [map_cast() for i in range(10**3)]
mean_map_cast = mean(map_cast_times)
stdev_map_cast = stdev(map_cast_times)
print(f"_map_castの平均値:{mean_map_cast}")
print(f"_map_castの標準偏差:{stdev_map_cast}")

hist.py
import pandas as pd
df = pd.DataFrame(zip(for_cast_times, map_cast_times), columns=["for_cast", "map_cast"])
df["for_cast"].hist(bins=100,color="red")
df["map_cast"].hist(bins=100, color="blue")
平均値 標準偏差
for_cast 0.015 0.003
map_cast 0.007 0.0011

ヒストグラム(青:mapでcast 赤:forでcast)
image.png

mapを使おう!

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

上記でmapは基本的に早いと説明したがpythonの実装ミスなのかnumpyの実装ミスなのかnumpy.float32にmapを適用しようとすると遅い。
いっそnumpy.float64に変換した方が良いほどだ

np_float32.py
from statistics import mean,stdev
from time import time
import numpy as np

def np_float32():
    check = np.arange(0.000001, 0.1, 0.000001) #生成されるデータはnumpy.float64型
    change = check.astype(np.float32)

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

np_float32_times = [np_float32() for i in range(10**3)]
mean_np_float32 = mean(np_float32_times)
stdev_np_float32 = stdev(np_float32_times)
print(f"np_float32の平均値:{mean_np_float32}")
print(f"np_float32の標準偏差:{stdev_np_float32}")

np_float64.py
from statistics import mean,stdev
from time import time
import numpy as np

def np_float64():
    check = np.arange(0.000001, 0.1, 0.000001)
    start=time()
    list(map(lambda x :x-1, check))
    return time()-start

np_float64_times = [np_float64() for i in range(10**3)]
mean_np_float64 = mean(np_float64_times)
stdev_np_float64 = stdev(np_float64_times)
print(f"np_float64の平均値:{mean_np_float64}")
print(f"np_float64の標準偏差:{stdev_np_float64}")

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

平均値 標準偏差
np_float32 0.228 0.0269
np_float64 0.039 0.0039

ヒストグラム(青:np_float64 赤:np_float32)
image.png

となる。以下に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
平均値 標準偏差
np_float32 0.228 0.0269
np_float64 0.039 0.0039
np_float64→np_float32 0.047 0.0068

ヒストグラム(青:np_float64 赤:np_float32, 緑:np_float64→np_float32)
分かりにくいが0.049付近の緑が
numpy.float32 → map → numpy.float64 → listの順で実行したものである
image.png

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

5.tupleを使おう!

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

listをappend.py
from statistics import mean,stdev
from time import time
import numpy as np

def list_object_append():
    random_list=np.random.rand(10**7)

    start=time()
    list_test=[ [i,i] for i in random_list]
    b=pd.DataFrame(list_test)
    return time()-start

list_object_append_times = [list_object_append() for i in range(10**3)]
mean_list_object_append = mean(list_object_append_times)
stdev_list_object_append = stdev(list_object_append_times)
print(f"list_object_appendの平均値:{mean_list_object_append}")
print(f"list_object_appendの標準偏差:{stdev_list_object_append}")

tupleをappend.py
from statistics import mean,stdev
from time import time
import numpy as np

def tuple_object_append():
    random_list=np.random.rand(10**7)

    start=time()
    tuple_test=((i,i) for i in random_list)
    a=pd.DataFrame(tuple_test)
    return time()-start

tuple_object_append_times = [tuple_object_append() for i in range(10**3)]
mean_tuple_object_append = mean(tuple_object_append_times)
stdev_tuple_object_append = stdev(tuple_object_append_times)
print(f"tuple_object_appendの平均値:{mean_tuple_object_append}")
print(f"tuple_object_appendの標準偏差:{stdev_tuple_object_append}")
平均値 標準偏差
listをappend 0.064 0.0109
tupleをappend 0.045 0.0054

ヒストグラム(青:tupleをappend 赤:listをappend)
image.png

何割か縮めることができる。配列のサイズによっても変わってくるのでグラフにする。
横軸は配列のサイズ、縦軸は処理時間の100回平均

配列のサイズ tuple list
10^5 0.042 0.067
10^6 0.447 0.717
10^7 4.661 6.162

image.png

まとめ

・whileは使わない
・リスト作るときにfor文使うなら内包表記を使おう
・if文の条件式を見直そう
・mapを使おう
・appendが必要な時にtupleが使えるなら使おう!

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

515
393
10

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
515
393