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


706

私は最近の処理速度を比較[]し、list()その発見に驚いた[]ランを超える3倍速くよりlist()。私はと同じテストを実行{}し、dict():との結果が実質的に同一であった[]{}しながら、両方は約0.128sec /万サイクルを取ったlist()し、dict()およそ0.428sec /万サイクルごとをしました。

どうしてこれなの?やる[]{}(そしておそらく()''、あまりにも)その明示的に名前のカウンターパートは、(一方で、すぐにいくつかの空の株式リテラルのコピーをバックパスlist()dict()tuple()str())は完全に彼らが実際の要素を持っているかどうか、オブジェクトの作成に取り掛かりますか?

私はこれら2つの方法がどのように違うのか分かりませんが、知りたいです。ドキュメントやSOで答えを見つけることができず、空の角かっこを検索すると、予想よりも問題が多いことがわかりました。

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

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


2
注:()''彼らは唯一の空じゃないとして、特別で、彼らは、そのように、それはそれらシングルトンを作るための簡単な勝利は不変ですね。彼らは新しいオブジェクトを構築することすらせず、空のtuple/のシングルトンをロードするだけstrです。技術的には実装の詳細ですが、パフォーマンス上の理由から 、なぜそれら空のtuple/ strをキャッシュしないのか想像するのに苦労します。あなたの直感だから[]{}株式リテラルをバック渡し間違っていたが、それはには適用されない()''
ShadowRanger 2018年

回答:


757

[]{}リテラル構文であるためです。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()は別のオブジェクトです。それらの名前を解決する必要があり、引数をプッシュするためにスタックを含める必要があり、後で取得するためにフレームを格納する必要があり、呼び出しを行う必要があります。それにはすべて時間がかかります。

空の場合は、少なくともa 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

したがって、オブジェクトを呼び出す必要がある場合、1.00 - 0.31 - 0.30 == 0.391,000万回の呼び出しごとにさらに1秒かかります。

グローバル名をローカルとしてエイリアスすることにより、グローバルルックアップコストを回避でき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コストを克服することはできません。


150

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

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

75

のではlistある機能ながら、リストオブジェクトに文字列を変換すると言う[]バットオフリストを作成するために使用されます。これを試してください(あなたにとってもっと意味があるかもしれません):

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

ながら

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

あなたがそれに入れたものをすべて含む実際のリストをあなたに与えます。


7
これは質問に直接対処するものではありません。問題は、なぜ[]がより速いのlist()かで['wham bam']はなく、なぜが速いのかということlist('wham bam')でした。
Jeremy Visser

2
ので、私にはほとんど意味をなさ@JeremyVisser []/ list()と全く同じである['wham']/ list('wham')彼らはちょうど同じ変数の違いを持っているので、1000/10同じである100/1数学インチ あなたは理論的には取り除くことができwham bam、事実は同じです。それlist()は、関数名を呼び出すことによって何かを変換しようとする一方で[]、変数を変換するだけです。関数呼び出しは異なります。はい。これは問題の論理的な概要にすぎません。たとえば、企業のネットワークマップもソリューション/問題の論理的なものです。好きなように投票してください。
2015

逆に、@ JeremyVisserは、コンテンツに対して異なる操作を行うことを示しています。
Baldrickk 2018

20

ここでの回答は素晴らしいです。要点はこの質問を完全にカバーしています。興味のある方のために、バイトコードからさらに一歩下げます。私はCPythonの最新のリポジトリを使用しています。古いバージョンはこの点で同様に動作しますが、若干の変更が加えられている可能性があります。

ここでは、BUILD_LISTfor []CALL_FUNCTIONforのそれぞれの実行の内訳を示しlist()ます。


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?いいえ、そうですlistlistタイプではありませんPyCFunction
  • PyMethodType?いいえ、前を参照してください。
  • PyFunctionType?いいえ、前を参照してください。

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

この関数は、特定の関数タイプ(理由は理解できません)をいくつかチェックし、必要に応じてkwargsのdictを作成した後、を呼び出します_PyObject_FastCallDict

_PyObject_FastCallDictついに私たちをどこかに連れて行きます!実行した後、さらにチェックを それはグラブtp_callからスロットtypetype我々はそれがつかむ、あること、に渡されましたtype.tp_call。次に、渡された引数からタプルを作成し_PyStack_AsTuple、最後に、最終的に呼び出しを行うことができます!

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

最後に、使用可能な引数を使用してリストをtype_call呼び出しlist.__init__、初期化してから、前の方法に戻ります。:-)

最後に、remmeberはLOAD_NAME、ここで貢献している別の人です。


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

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

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

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


13

なぜ[]より速いのです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] ")"

リストだけでなく、どの名前でも同じことができることがわかります。

>>> 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()呼び出し可能なためルックスと、それを呼び出します。

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


2
+1は、ハイジャックできることlist、およびPythonコンパイラが本当に空のリストを返すかどうかを確認できないことを指摘します。
ビーフスター
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.