私が理解しているのは、コードを高速化する必要はなく、なぜ低速なのかを知りたいだけだということです。それに答えるために、分解を見てみましょう。この説明のために、関数呼び出しで各メソッドをラップします。各逆アセンブリでは、およびリターン コマンドのロードはu
無視v
できます。
def test1(u, v):
return (u[0]-v[0])**2 + (u[1]-v[1])**2 + (u[3]-v[3])**2
dis.dis(test1)
0 LOAD_FAST 0 (u)
3 LOAD_CONST 1 (0)
6 BINARY_SUBSCR
7 LOAD_FAST 1 (v)
10 LOAD_CONST 1 (0)
13 BINARY_SUBSCR
14 BINARY_SUBTRACT
15 LOAD_CONST 2 (2)
18 BINARY_POWER
19 LOAD_FAST 0 (u)
22 LOAD_CONST 3 (1)
25 BINARY_SUBSCR
26 LOAD_FAST 1 (v)
29 LOAD_CONST 3 (1)
32 BINARY_SUBSCR
33 BINARY_SUBTRACT
34 LOAD_CONST 2 (2)
37 BINARY_POWER
38 BINARY_ADD
39 LOAD_FAST 0 (u)
42 LOAD_CONST 4 (3)
45 BINARY_SUBSCR
46 LOAD_FAST 1 (v)
49 LOAD_CONST 4 (3)
52 BINARY_SUBSCR
53 BINARY_SUBTRACT
54 LOAD_CONST 2 (2)
57 BINARY_POWER
58 BINARY_ADD
59 RETURN_VALUE
同じパターンを何度も繰り返すだけなので、最初の例は長さ 3 で切り捨てました。関数呼び出しのオーバーヘッドがなく、ほとんどの場合、インタープリターはこれらのオペランドに対して可能な限り最小限の作業を行って結果を達成していることがわかります。
def test2(u, v):
sum((a-b)**2 for a, b in izip(u, v))
dis.dis(test2)
0 LOAD_GLOBAL 0 (sum)
3 LOAD_CONST 1 (<code object <genexpr> at 02C6F458, file "<pyshell#10>", line 2>)
6 MAKE_FUNCTION 0
9 LOAD_GLOBAL 1 (izip)
12 LOAD_FAST 0 (u)
15 LOAD_FAST 1 (v)
18 CALL_FUNCTION 2
21 GET_ITER
22 CALL_FUNCTION 1
25 CALL_FUNCTION 1
28 RETURN_VALUE
ここでわかるのは、ジェネレーター式から関数を作成し、2 つのグローバル (sum と izip) をロードすることです。グローバル ルックアップはローカル ルックアップよりも遅くなります。一度作成することは避けられませんが、それらが呼び出される場合タイトなループでは、多くの人がそれらを_izip
orなどのローカルに割り当て_sum
、その後 izip を呼び出し、ジェネレーターからイテレーターを取得し、ジェネレーターによって作成された関数を呼び出し、次にsum 関数 (イテレータを消費し、返す前に各項目を追加します)。
def test3(u, v):
sum = 0
for i in xrange(len(u)):
sum += (u[i]-v[i])**2
dis.dis(test3)
0 LOAD_CONST 1 (0)
3 STORE_FAST 2 (sum)
6 SETUP_LOOP 52 (to 61)
9 LOAD_GLOBAL 0 (xrange)
12 LOAD_GLOBAL 1 (len)
15 LOAD_FAST 0 (u)
18 CALL_FUNCTION 1
21 CALL_FUNCTION 1
24 GET_ITER
25 FOR_ITER 32 (to 60)
28 STORE_FAST 3 (i)
31 LOAD_FAST 2 (sum)
34 LOAD_FAST 0 (u)
37 LOAD_FAST 3 (i)
40 BINARY_SUBSCR
41 LOAD_FAST 1 (v)
44 LOAD_FAST 3 (i)
47 BINARY_SUBSCR
48 BINARY_SUBTRACT
49 LOAD_CONST 2 (2)
52 BINARY_POWER
53 INPLACE_ADD
54 STORE_FAST 2 (sum)
57 JUMP_ABSOLUTE 25
60 POP_BLOCK
61 LOAD_CONST 0 (None)
64 RETURN_VALUE
ここに表示されているのは、 で起こっていることのより単純なバージョンですtest2
。xrange(len(u))
ジェネレータ式や sum の呼び出しはありませんが、@Lucas Malor によって提案されたより高速なソリューションの代わりに、関数呼び出しのオーバーヘッドを不要な関数呼び出しに置き換えました。
def test4(u, v):
mysum = 0
for a, b in izip(u, v) :
mysum += (a-b)**2
return mysum
dis.dis(test4)
0 LOAD_CONST 1 (0)
3 STORE_FAST 2 (mysum)
6 SETUP_LOOP 47 (to 56)
9 LOAD_GLOBAL 0 (izip)
12 LOAD_FAST 0 (u)
15 LOAD_FAST 1 (v)
18 CALL_FUNCTION 2
21 GET_ITER
22 FOR_ITER 30 (to 55)
25 UNPACK_SEQUENCE 2
28 STORE_FAST 3 (a)
31 STORE_FAST 4 (b)
34 LOAD_FAST 2 (mysum)
37 LOAD_FAST 3 (a)
40 LOAD_FAST 4 (b)
43 BINARY_SUBTRACT
44 LOAD_CONST 2 (2)
47 BINARY_POWER
48 INPLACE_ADD
49 STORE_FAST 2 (mysum)
52 JUMP_ABSOLUTE 22
55 POP_BLOCK
56 LOAD_FAST 2 (mysum)
59 RETURN_VALUE
上記は@Lucas Malorの貢献を表しており、いくつかの点で高速です。これは、呼び出しの数を 1 に減らしながら、添字操作をアンパックに置き換えます。これは、多くの場合、指定した制約で達成するのと同じくらい高速です。
eval
オーバーヘッドに見合うだけの回数関数を呼び出す場合は、test1 の関数と同様のランタイム生成文字列を使用するだけの価値があることに注意してください。また、u と v の長さがますます大きくなるにつれて (これは通常、このタイプのアルゴリズムが評価される方法です)、他のソリューションの関数呼び出しのオーバーヘッドがますます小さくなるため、ほとんどの場合、最も読みやすいソリューションが非常に優れていることに注意してください。 . 同時に、小さなケースでは遅くなりますが、シーケンス u と v の長さが非常に長い場合は、リスト内包表記ではなくジェネレーター式をお勧めします。メモリの節約により、ほとんどの場合、実行が大幅に高速化されます (および gc が高速化されます)。
全体として、短いシーケンスの場合の小さなスピードアップは、コードサイズの増加と、マイクロ最適化を実行することによって見ている他の Python の実装との一貫性のない動作に値しないということをお勧めします。「最良の」解決策は、ほぼ間違いなくtest2
.