web-dev-qa-db-ja.com

なぜ中間変数を使用するコードは、なしのコードよりも高速ですか?

私はこの奇妙な振る舞いに遭遇し、それを説明できませんでした。これらはベンチマークです。

_py -3 -m timeit "Tuple(range(2000)) == Tuple(range(2000))"
10000 loops, best of 3: 97.7 usec per loop
py -3 -m timeit "a = Tuple(range(2000));  b = Tuple(range(2000)); a==b"
10000 loops, best of 3: 70.7 usec per loop
_

なぜ変数割り当てとの比較が、一時変数を含む1つのライナーを使用するよりも27%以上速いのでしょうか?

Pythonドキュメントでは、ガベージコレクションはtimeitの間無効になっているため、それは不可能です。何らかの最適化ですか?

結果は、Python 2.xでも再現できますが、程度は低いです。

Windows 7、CPython 3.5.1、Intel i7 3.40 GHz、64ビットOSおよびPythonの両方を実行。 Intel i7 3.60 GHzでPython 3.5.0は結果を再現しません。


同じPythonプロセスでtimeit.timeit() @ 10000ループを使用してそれぞれ0.703と0.804を生成しました。程度は低いものの、まだ表示されています。(12.5%)

76
Bharel

私の結果はあなたのものに似ていました:中間変数を使用するコードは、Python 3.4で少なくとも一貫して少なくとも10-20%高速でした。しかし、同じPython 3.4インタプリタ、私はこれらの結果を得ました:

In [1]: %timeit -n10000 -r20 Tuple(range(2000)) == Tuple(range(2000))
10000 loops, best of 20: 74.2 µs per loop

In [2]: %timeit -n10000 -r20 a = Tuple(range(2000));  b = Tuple(range(2000)); a==b
10000 loops, best of 20: 75.7 µs per loop

特に、コマンドラインから-mtimeitを使用した場合、前者の74.2 µsに近づくことさえできませんでした。

そのため、このハイゼンバグは非常に興味深いものであることが判明しました。私はstraceを指定してコマンドを実行することにしましたが、実際、何か怪しいことが起こっています。

% strace -o withoutvars python3 -m timeit "Tuple(range(2000)) == Tuple(range(2000))"
10000 loops, best of 3: 134 usec per loop
% strace -o withvars python3 -mtimeit "a = Tuple(range(2000));  b = Tuple(range(2000)); a==b"
10000 loops, best of 3: 75.8 usec per loop
% grep mmap withvars|wc -l
46
% grep mmap withoutvars|wc -l
41149

これが違いの正当な理由です。変数を使用しないコードにより、mmapシステムコールは、中間変数を使用するコードよりもほぼ1000倍呼び出されます。

withoutvarsは、256kリージョンのmmap/munmapでいっぱいです。これらの同じ行が何度も繰り返されます。

mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144)          = 0

mmap呼び出しは、関数_PyObject_ArenaMmap from Objects/obmalloc.cから来ているようです。 obmalloc.cにはマクロARENA_SIZEも含まれます。これは#definedで(256 << 10)(つまり262144)になります。同様に、munmap_PyObject_ArenaMunmapobmalloc.cと一致します。

obmalloc.cは言う

Python 2.5より前、アリーナはfree() 'edになることはありませんでした。Python 2.5から、free() arenasを試してみて、いくつかの穏やかな発見的戦略を使用してアリーナが最終的に解放される可能性。

したがって、これらのヒューリスティックとPythonオブジェクトアロケーターが空になるとすぐにこれらの無料アリーナを解放するという事実は、1つの256 kiBのメモリ領域が再割り当てされて繰り返し解放される病理学的動作を引き起こすpython3 -mtimeit 'Tuple(range(2000)) == Tuple(range(2000))'につながります。この割り当てはmmap/munmapで発生します。これはシステムコールであるため比較的コストがかかります-さらに、mmap with MAP_ANONYMOUSでは、新しくマップされたページをゼロにする必要があります-たとえPythonは気にしません。

動作は中間変数を使用するコードには存在しません。これは、わずかにmoreメモリを使用しており、一部のオブジェクトがまだ残っているためメモリ領域を解放できないためです。それに割り当てられます。それは、timeitがそうではないループにそれを作るからです

for n in range(10000)
    a = Tuple(range(2000))
    b = Tuple(range(2000))
    a == b

現在の動作では、abの両方が*再割り当てされるまでバインドされたままなので、2回目の反復でTuple(range(2000))が3番目のタプルを割り当て、a = Tuple(...)の割り当てにより参照カウントが減少しますリリースされた古いタプル、および新しいタプルの参照カウントを増やします。同じことがbにも起こります。したがって、最初の反復の後、これらのタプルは少なくとも3つではなく、常に2つあるため、スラッシングは発生しません。

最も注目すべきは、中間変数を使用するコードが常に高速であることを保証できないことです。実際、一部のセットアップでは、中間変数を使用すると余分なmmap呼び出しが発生する場合がありますが、戻り値を直接比較するコードは問題ない場合があります。


timeitがガベージコレクションを無効にしているときに、なぜこれが起こるのかと尋ねられました。 timeitはそれを行う

デフォルトでは、timeit()はタイミング中にガベージコレクションを一時的にオフにします。このアプローチの利点は、独立したタイミングをより比較可能にすることです。この欠点は、GCが測定対象の機能のパフォーマンスの重要な要素になる可能性があることです。その場合、セットアップ文字列の最初のステートメントとしてGCを再度有効にすることができます。例えば:

ただし、Python)のガベージコレクタは、サイクリックガベージ、つまり参照がサイクルを形成するオブジェクトのコレクションを回収するためにのみ存在します。そうではありません。ここでは、代わりにこれらのオブジェクトは、参照カウントがゼロに下がるとすぐに解放されます。

106
Antti Haapala

ここでの最初の質問は、再現可能である必要がありますか?少なくとも一部の人にとっては、間違いなく、他の人は効果が見られないと言っています。これはFedoraで、実際に比較を行うことで結果が無関係であるように等価テストがisに変更され、効果を最大化するように範囲が200,000に押し上げられました。

$ python3 -m timeit "a = Tuple(range(200000));  b = Tuple(range(200000)); a is b"
100 loops, best of 3: 7.03 msec per loop
$ python3 -m timeit "a = Tuple(range(200000)) is Tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "Tuple(range(200000)) is Tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "a = b = Tuple(range(200000)) is Tuple(range(200000))"
100 loops, best of 3: 9.99 msec per loop
$ python3 -m timeit "a = b = Tuple(range(200000)) is Tuple(range(200000))"
100 loops, best of 3: 10.2 msec per loop
$ python3 -m timeit "Tuple(range(200000)) is Tuple(range(200000))"
100 loops, best of 3: 10.1 msec per loop
$ python3 -m timeit "a = Tuple(range(200000));  b = Tuple(range(200000)); a is b"
100 loops, best of 3: 7 msec per loop
$ python3 -m timeit "a = Tuple(range(200000));  b = Tuple(range(200000)); a is b"
100 loops, best of 3: 7.02 msec per loop

実行間の違い、および式の実行順序は結果にほとんど影響しないことに注意してください。

abへの割り当てを低速バージョンに追加しても、速度は上がりません。実際、ローカル変数への代入の効果はごくわずかです。速度を上げる唯一の方法は、式を完全に2つに分割することです。これが行うべき唯一の違いは、式を評価するときにPythonが使用する最大スタック深度を減らす(4から3))ことです。

これは、効果がスタックの深さに関連しているという手がかりを与えます。おそらく、余分なレベルがスタックを別のメモリページにプッシュします。もしそうなら、スタックに影響を与える他の変更を加えると、変更される可能性があります(ほとんどの場合、効果を殺します)。

$ python3 -m timeit -s "def foo():
   Tuple(range(200000)) is Tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
   Tuple(range(200000)) is Tuple(range(200000))" "foo()"
100 loops, best of 3: 10 msec per loop
$ python3 -m timeit -s "def foo():
   a = Tuple(range(200000));  b = Tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 9.97 msec per loop
$ python3 -m timeit -s "def foo():
   a = Tuple(range(200000));  b = Tuple(range(200000)); a is b" "foo()"
100 loops, best of 3: 10 msec per loop

ですから、効果は完全にタイミングプロセス中にスタックが消費されるPythonによるものだと思います。それでもなお奇妙です。

7
Duncan