web-dev-qa-db-ja.com

なぜlist()よりも速いのですか?

私は最近[]list()の処理速度を比較しましたが、[] 3倍以上速い list()よりも速いことを発見して驚きました。私は{}dict()で同じテストを実行しました、そして結果は実質的に同一でした:[]{}は両方ともおよそ0.128秒/百万サイクルかかりましたが、list()dict()はそれぞれおよそ0.428秒/百万サイクルかかりました。

どうしてこれなの? []{}(そしておそらく()''も)は、それらの明示的に名付けられた対応物(list()dict()Tuple()str())が完全にオブジェクトを作成するかどうかにかかわらず、ただちに空のストックリテラルのコピーを直ちに返す。要素がありますか?

これら2つの方法がどのように違うのかわかりませんが、調べたいと思います。私はドキュメントやSOで答えを見つけることができませんでした、そして空の括弧を探すことは私が思っていたよりもっと問題が多いことが判明しました。

リストと辞書を比較するために、それぞれtimeit.timeit("[]")timeit.timeit("list()")、そしてtimeit.timeit("{}")timeit.timeit("dict()")を呼び出すことでタイミング結果を得ました。私はPython 2.7.9を使っています。

私は最近if Trueのパフォーマンスとif 1のパフォーマンスを比較する「 なぜTrueは1より遅いのですか? 」を発見しました。これは同様のリテラル対グローバルのシナリオに触れているようです。おそらくそれも考慮する価値があります。

646
Augusta

[]{} リテラル構文 です。 Pythonはリストや辞書オブジェクトを作成するためだけにバイトコードを作成することができます。

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

list()dict()は別々のオブジェクトです。それらの名前は解決される必要があり、スタックは引数をプッシュするために関与しなければならず、フレームは後で検索するために格納されなければならず、そして呼び出しが行われなければなりません。それはすべて時間がかかります。

空の場合は、少なくとも LOAD_NAME (グローバル名前空間と __builtin__モジュール を検索する必要があります)の後に CALL_FUNCTION があります。現在のフレームを保存するには:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

timeitを使用して名前検索を別々に計時できます。

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

時間の不一致はおそらく辞書ハッシュ衝突です。それらのオブジェクトを呼び出すための時間からそれらの時間を減算し、リテラルを使用するための時間と結果を比較します。

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

そのため、オブジェクトを呼び出さなければならない場合は、1000万回の呼び出しごとにさらに1.00 - 0.31 - 0.30 == 0.39秒かかります。

グローバル名をローカル名としてエイリアスすることで、グローバル検索のコストを避けることができます(timeit設定を使用し、名前にバインドするものはすべてローカルです)。

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

しかし、あなたはそのCALL_FUNCTIONのコストを克服することは決してできません。

703
Martijn Pieters

list()はグローバルルックアップと関数呼び出しを必要としますが、[]は単一の命令にコンパイルします。見る:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None
137
Dan D.

なぜならlistfunction で文字列をリストオブジェクトに変換するのに対し、[]はリストからリストを作成するのに使われるからです。これを試してください(あなたにとってより理にかなっているかもしれません):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

しながら

y = ["wham bam"]
>>> y
["wham bam"]

あなたがそれに入れたものは何でも含んでいる実際のリストをあなたに与えます。

73
Torxed

ここでの答えは、要点まで大きく、この質問を完全にカバーしています。興味のある人のために、バイトコードからさらに一歩下がっていきます。私はCPythonの最新のリポジトリを使用しています。この点で古いバージョンは同様に動作しますが、わずかな変更が行われる可能性があります。

以下に、これらのそれぞれの実行の内訳を示します。BUILD_LIST[]CALL_FUNCTIONlist()です。


BUILD_LIST命令:

あなたはただ恐怖を見るべきです:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
Push(list);
DISPATCH();

ひどく複雑です、私は知っています。これは非常に簡単です。

  • PyList_New (主に新しいリストオブジェクトにメモリを割り当てる)で新しいリストを作成し、opargがスタック上の引数の数を通知します。まっすぐに。
  • if (list==NULL)に問題がなかったことを確認します。
  • PyList_SET_ITEM (マクロ)でスタックにある引数(この場合は実行されません)を追加します。

それが速いのも不思議ではありません!新しいリストを作成するためのカスタムメイドで、他には何もありません:-)

CALL_FUNCTION命令:

CALL_FUNCTIONを処理するコードを覗いたときに最初に目にするのは次のとおりです。

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
Push(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

かなり無害に見えますよね?まあ、いや、残念ながらそうではありません。 call_function は、関数をすぐに呼び出す簡単な男ではありません。代わりに、スタックからオブジェクトを取得し、スタックのすべての引数を取得してから、オブジェクトのタイプに基づいて切り替えます。それは:

  • PyCFunction_Type ?いいえ、それはlistです、listはタイプPyCFunctionではありません
  • PyMethodType ?いいえ、前を参照してください。
  • PyFunctionType ?いいえ、前を参照してください。

list型を呼び出しています。call_functionに渡される引数は PyList_Type です。 CPythonは、ジェネリック関数を呼び出して _PyObject_FastCallKeywords という名前の呼び出し可能なオブジェクトを処理する必要があります。

この関数は、特定の関数タイプに対していくつかのチェックを行い(理由はわかりません)、kwargsの辞書を作成した後必要な場合_PyObject_FastCallDictを呼び出します。

_PyObject_FastCallDictが最終的にどこかに到達します!さらにチェックを実行した後、それは 渡したtypetype からtp_callスロットを取得します。つまり、type.tp_callを取得します。次に、_PyStack_AsTupleで渡された引数からTupleを作成し、最後に呼び出しが最終的に行われる

tp_call に一致するtype.__call__が引き継ぎ、最終的にリストオブジェクトを作成します。 __new__ に対応するリストPyType_GenericNewを呼び出し、 PyType_GenericAlloc でメモリを割り当てます:これは実際にPyList_Newに追いつく部分です。最後に。オブジェクトを一般的な方法で処理するには、上記のすべてが必要です。

最後に、type_calllist.__init__を呼び出し、使用可能な引数でリストを初期化してから、元の状態に戻ります。 :-)

最後に、LOAD_NAMEを思い出してください。これはここで貢献しているもう1人の男です。


入力を処理するとき、Pythonは通常、フープをジャンプして、ジョブを実行するための適切なC関数を実際に見つける必要があることは簡単にわかります。それは動的であるため、すぐに呼び出すという礼儀はありません。誰かがlistをマスクし、多くの人がやる)可能性があり、別の道をとらなければなりません.

これはlist()が多くを失うところです:探索するPythonは一体何をすべきかを知るために行う必要があります。

一方、リテラル構文は、1つのことを意味します。変更することはできず、常に所定の方法で動作します。

脚注:すべての関数名は、リリースごとに変更される場合があります。重要な点はまだあり、おそらく将来のバージョンでも有効です。それは、物事を遅くする動的なルックアップです。

[]list()より速いのはなぜですか?

最大の理由は、Pythonがlist()をユーザー定義関数のように扱うということです。つまり、何か他のものをlistにエイリアスして別のことをすることで傍受できるということです。

それはすぐに[]で組み込みリストの新しいインスタンスを作成します。

私の説明はあなたにこれのための直感を与えるように努める。

説明

[]は一般にリテラル構文として知られています。

文法上、これは「リスト表示」と呼ばれます。 ドキュメントから

リスト表示は、角括弧で囲まれた一連の空の式です。

list_display ::=  "[" [starred_list | comprehension] "]"

リスト表示は新しいリストオブジェクトを生成し、その内容は式のリストまたは内包表記によって指定されます。コンマで区切られた式のリストが提供されると、その要素は左から右に評価され、リストオブジェクトにその順序で配置されます。内包表記が供給されると、リストは内包表記の結果の要素から構成されます。

つまり、list型の組み込みオブジェクトが作成されるということです。

これを迂回することはありません。つまり、Pythonができる限り早くそれを実行できるということです。

一方、list()は組み込みリストコンストラクタを使って組み込みlistを作成することから傍受することができます。

たとえば、リストを騒々しく作成したいとします。

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

その後、モジュールレベルのグローバルスコープでlistという名前をインターセプトし、listを作成するときに、実際にサブタイプリストを作成します。

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

同様に、グローバル名前空間から削除することもできます

del list

そしてそれを組み込み名前空間に入れます。

import builtins
builtins.list = List

そしていま:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

また、リスト表示は無条件にリストを作成します。

>>> list_1 = []
>>> type(list_1)
<class 'list'>

おそらくこれは一時的なものなので、変更を元に戻すことができます - 最初に組み込みから新しいListオブジェクトを削除します。

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined

ああ、いや、原本を見失った。

心配しないでください、私達はまだlistを手に入れることができます - それはリストリテラルの型です:

>>> builtins.list = type([])
>>> list()
[]

そう...

[]list()より速いのはなぜですか?

これまで見てきたようにlistを上書きすることはできますが、リテラル型の作成を傍受することはできません。 listを使うとき、何かがあるかどうかを調べるために検索をしなければなりません。

それから私たちが調べたどんな呼び出し可能オブジェクトでも呼び出す必要があります。文法から:

呼び出しは、空の可能性がある一連の引数を使用して呼び出し可能オブジェクト(たとえば、関数)を呼び出します。

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"

Listだけでなく、どの名前に対しても同じことができることがわかります。

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

[]には、Pythonバイトコードレベルでの関数呼び出しはありません。

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

単にバイトコードレベルで検索や呼び出しを行わずにリストを作成するだけです。

結論

スコープルールを使用してlistをユーザーコードでインターセプトできること、そしてlist()が呼び出し可能オブジェクトを探してそれを呼び出すことを示しました。

一方、[]はリスト表示またはリテラルであるため、名前の検索と関数呼び出しは避けます。

10
Aaron Hall