過去数日間、私はpython関数の実行時間の改善に取り組んできました。これは、特に残余関数(%)の多くの使用を必要とします。私の主なテストケースは80,000以上です。要素のnumpy配列(単調増加)、10000回の反復、他のさまざまなサイズでも試しました。
最終的に、残りの機能が主要なボトルネックになるポイントに到達し、さまざまなソリューションを試しました。これは、次のコードを実行したときに見つかった動作です。
import numpy as np
import time
a = np.random.Rand(80000)
a = np.cumsum(a)
d = 3
start_time1 = time.time()
for i in range(10000):
b = a % d
d += 0.001
end_time1 = time.time()
d = 3
start_time2 = time.time()
for i in range(10000):
b = a - (d * np.floor(a / d))
d += 0.001
end_time2 = time.time()
print((end_time1 - start_time1) / 10000)
print((end_time2 - start_time2) / 10000)
出力は次のとおりです。
0.0031344462633132934
0.00022937238216400147
配列サイズを800,000に増やす場合:
0.014903099656105041
0.010498356819152833
(この投稿では、実際の出力のためにコードを1回だけ実行しましたが、これらの結果を一貫して得た問題を理解しようとしました。)
これでランタイムの問題は解決しますが、その理由を理解するのに苦労しています。何か不足していますか?私が考えることができる唯一の違いは、追加の関数呼び出しのオーバーヘッドですが、最初のケースはかなり極端です(そして、1.5倍のランタイムでも十分ではありません)。 np.remainder
関数は無意味です。
編集:非ヌルループで同じコードをテストしてみました:
import numpy as np
import time
def Pythonic_remainder(array, d):
b = np.zeros(len(array))
for i in range(len(array)):
b[i] = array[i] % d
def split_Pythonic_remainder(array, d):
b = np.zeros(len(array))
for i in range(len(array)):
b[i] = array[i] - (d * np.floor(array[i] / d))
def split_remainder(a, d):
return a - (d * np.floor(a / d))
def divide(array, iterations, action):
d = 3
for i in range(iterations):
b = action(array, d)
d += 0.001
a = np.random.Rand(80000)
a = np.cumsum(a)
start_time = time.time()
divide(a, 10000, split_remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, np.remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, Pythonic_remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, split_Pythonic_remainder)
print((time.time() - start_time) / 10000)
私が得る結果は次のとおりです。
0.0003770533800125122
0.003932329940795899
0.018835473942756652
0.10940513386726379
興味深いのは、非数値の場合には反対のことが当てはまることです。
私の最良の仮説は、NumPyのインストールが%
計算内で最適化されていないfmod
を使用していることです。その理由は次のとおりです。
まず、NumPy 1.15.1の通常のpipインストールバージョンでは結果を再現できません。私は約10%のパフォーマンスの違いしか得られません(asdf.pyにはタイミングコードが含まれています):
$ python3.6 asdf.py
0.0006543657302856445
0.0006025806903839111
Icanは、NumPy Gitリポジトリのクローンからのv1.15.1の手動ビルド(python3.6 setup.py build_ext --inplace -j 4
)との主要なパフォーマンスの不一致を再現します。 :
$ python3.6 asdf.py
0.00242799973487854
0.0006397026300430298
これは、私のpipインストールビルドの%
が、手動ビルドまたはインストールしたものよりも最適化されていることを示唆しています。
内部を見ると、NumPyの浮動小数点%
の- 実装 を見て、 不要なfloordiv計算 (npy_divmod@c@
は//
と%
の両方を計算します):
NPY_NO_EXPORT void
@TYPE@_remainder(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
{
BINARY_LOOP {
const @type@ in1 = *(@type@ *)ip1;
const @type@ in2 = *(@type@ *)ip2;
npy_divmod@c@(in1, in2, (@type@ *)op1);
}
}
しかし、私の実験では、floordivを削除してもメリットはありませんでした。コンパイラーが最適化するのは簡単に見えるので、最適化されたのか、そもそもランタイムのごくわずかな部分だったのかもしれません。
Floordivではなく、npy_divmod@c@
、fmod
呼び出しの1行だけに注目しましょう。
mod = npy_fmod@c@(a, b);
これは、特別な場合の処理および結果を調整して右側のオペランドの符号と一致させる前の、最初の剰余計算です。手動ビルドで%
とnumpy.fmod
のパフォーマンスを比較すると:
>>> import timeit
>>> import numpy
>>> a = numpy.arange(1, 8000, dtype=float)
>>> timeit.timeit('a % 3', globals=globals(), number=1000)
0.3510419335216284
>>> timeit.timeit('numpy.fmod(a, 3)', globals=globals(), number=1000)
0.33593094255775213
>>> timeit.timeit('a - 3*numpy.floor(a/3)', globals=globals(), number=1000)
0.07980139832943678
fmod
が%
のランタイム全体のほとんどを担当しているようです。
生成されたバイナリを逆アセンブルしたり、命令レベルのデバッガーでステップ実行して正確に実行されるものを確認したりしておらず、もちろん、yourマシンまたはNumPyのコピー。それでも、上記の証拠から、fmod
はかなり犯人のようです。