5

タプルで表される 2 つのベクトル間のユークリッド距離を計算しています。

(u[0]-v[0])**2 + (u[1]-v[1])**2 + (u[3]-v[3])**2 ...

これを行うためのハードコーディングされた方法は非常に高速です。ただし、これらのベクトルの長さについては何も仮定したくありません。その結果、次のようなソリューションが得られます。

sum([(a-b)**2 for a, b in izip(u, v)]) # Faster without generator

また

sum = 0
for i in xrange(len(u)):
    sum += (u[i]-v[i])**2

これは、最初のバージョンよりもはるかに (少なくとも 2 倍) 遅いことが判明しました。NumPy/SciPy に頼らずに、これを最適化するスマートな方法はありますか? これらのパッケージがそのようなことを行うための最速の方法を提供していることは承知していますが、現時点では、「裸の Python」を最適化する経験を積もうとしています。私が見つけたのは、関数とexec()それを定義する文字列を動的に構築することですが、それは本当に最後の手段です...

要求事項:

  • CPython 2.7
  • 標準ライブラリのみ
  • "Real" (例: no exec())、純粋な Python

私の質問は一般的な小さな操作の問題に関するものですが、ソリューションでは、ベクトルの1つが複数の関数呼び出しで同じままであると想定する場合があります。

4

2 に答える 2

2
mysum = 0
for a, b in izip(u, v) :
    mysum += (a-b)**2

#3より約35%高速

PS: Cython (CPython ではない) またはShedskinを試しましたか?

于 2013-03-19T21:50:55.250 に答える
1

私が理解しているのは、コードを高速化する必要はなく、なぜ低速なのかを知りたいだけだということです。それに答えるために、分解を見てみましょう。この説明のために、関数呼び出しで各メソッドをラップします。各逆アセンブリでは、およびリターン コマンドのロードは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) をロードすることです。グローバル ルックアップはローカル ルックアップよりも遅くなります。一度作成することは避けられませんが、それらが呼び出される場合タイトなループでは、多くの人がそれらを_iziporなどのローカルに割り当て_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

ここに表示されているのは、 で起こっていることのより単純なバージョンですtest2xrange(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.

于 2013-03-25T16:27:53.567 に答える