web-dev-qa-db-ja.com

Pythonセット作成のパフォーマンス比較-set()vs. {}リテラル

この質問 に続く議論に疑問が残るので、いくつかのテストを実行して、set((x,y,z))と_{x,y,z}_の作成時間を比較してPython(私はPython 3.7を使用しています)。

timetimeitを使用して2つの方法を比較しました。両方とも以下の結果と一致していました*。

_test1 = """
my_set1 = set((1, 2, 3))
"""
print(timeit(test1))
_

結果:0.3024073549999999

_test2 = """
my_set2 = {1,2,3}
"""
print(timeit(test2))
_

結果:0.107717959000000

したがって、2番目の方法は、1番目の方法よりもほぼ3倍高速でした。これは私にとって驚くべき違いでした。このような方法でset()メソッドを介してセットリテラルのパフォーマンスを最適化するために、フードの下で何が起こっていますか?どのケースにどちらがお勧めですか?

*注:平均値なので、timeitテストの結果のみを表示します多くのサンプルで、したがっておそらくより信頼性が高いが、timeでテストした場合の結果は、両方のケースで同様の違いを示した。


編集:この同様の質問 を知っていますが、元の質問の特定の側面に答えますが、すべてをカバーします。セットは質問で扱われていませんでした、空のセットはPythonでリテラル構文を持っていないので、(もしあれば)セットの作成に興味がありましたリテラルの使用は、set()メソッドの使用とは異なります。また、set((x,y,z)の-​​Tuple parameterの処理がバックグラウンドでどのように行われ、ランタイムにどのような影響があるのか​​疑問に思いました。 coldspeedのすばらしい回答は、問題を解決するのに役立ちました。

18
yuvgin

(これは、最初の質問から編集されたコードへの応答です)2番目のケースで関数を呼び出すのを忘れました。適切な変更を加えると、結果は予想どおりになります。

_test1 = """
def foo1():
     my_set1 = set((1, 2, 3))
foo1()
"""    
timeit(test1)
# 0.48808742000255734
_
_test2 = """
def foo2():
    my_set2 = {1,2,3}
foo2()
"""    
timeit(test2)
# 0.3064506609807722
_

タイミングの違いの理由は、set()はシンボルテーブルのルックアップを必要とする関数呼び出しであるのに対し、_{...}_ set構文は構文のアーティファクトであり、はるかに高速であるためです。 。

逆アセンブルされたバイトコードを観察すると、違いは明らかです。

_import dis

dis.dis("set((1, 2, 3))")
  1           0 LOAD_NAME                0 (set)
              2 LOAD_CONST               3 ((1, 2, 3))
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
_
_dis.dis("{1, 2, 3}")
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 LOAD_CONST               2 (3)
              6 BUILD_SET                3
              8 RETURN_VALUE
_

最初のケースでは、関数呼び出しは、タプル_CALL_FUNCTION_の命令_(1, 2, 3)_によって行われます(これは、マイナーではありますが、独自のオーバーヘッドも伴います。これは_LOAD_CONST_を介して定数としてロードされます) )、2番目の命令では_BUILD_SET_呼び出しのみであるため、より効率的です。

再:タプルの構築にかかった時間に関するあなたの質問、これは実際には無視できると私たちは見ます:

_timeit("""(1, 2, 3)""")
# 0.01858693000394851

timeit("""{1, 2, 3}""")
# 0.11971827200613916
_

タプルは不変であるため、コンパイラーは定数としてロードすることでこの操作を最適化します。これは 定数の折りたたみ と呼ばれます(上記の_LOAD_CONST_命令から明確に確認できます)。無視できます。これは、セットが可変であるため、セットでは見られません(これを指摘してくれた@ user2357112に感謝します)。


大きなシーケンスの場合、同様の動作が見られます。 _{..}_構文は、ジェネレーターからセットを構築する必要があるset()とは対照的に、セット内包表記を使用してセットを構築する際に高速です。

_timeit("""set(i for i in range(10000))""", number=1000)
# 0.9775058150407858

timeit("""{i for i in range(10000)}""", number=1000)
# 0.5508635920123197
_

参考のために、より新しいバージョンで反復可能なアンパックを使用することもできます。

_timeit("""{*range(10000)}""", number=1000)
# 0.7462548640323803
_

興味深いことに、rangeで直接呼び出された場合、set()は高速です。

_timeit("""set(range(10000))""", number=1000)
# 0.3746800610097125
_

これは、たまたまセットの構築よりも高速です。他のシーケンス(listsなど)でも同様の動作が見られます。

私の推奨事項は、セットリテラルを構築するときに_{...}_セット内包表記を使用すること、およびジェネレータ内包表記をset();に渡す代わりに使用することです。代わりにset()を使用して、既存のシーケンス/イテラブルをセットに変換します。

31
cs95