私は最近[]
と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より遅いのですか? 」を発見しました。これは同様のリテラル対グローバルのシナリオに触れているようです。おそらくそれも考慮する価値があります。
[]
と{}
は リテラル構文 です。 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
のコストを克服することは決してできません。
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
なぜならlist
は function で文字列をリストオブジェクトに変換するのに対し、[]
はリストからリストを作成するのに使われるからです。これを試してください(あなたにとってより理にかなっているかもしれません):
x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]
しながら
y = ["wham bam"]
>>> y
["wham bam"]
あなたがそれに入れたものは何でも含んでいる実際のリストをあなたに与えます。
ここでの答えは、要点まで大きく、この質問を完全にカバーしています。興味のある人のために、バイトコードからさらに一歩下がっていきます。私はCPythonの最新のリポジトリを使用しています。この点で古いバージョンは同様に動作しますが、わずかな変更が行われる可能性があります。
以下に、これらのそれぞれの実行の内訳を示します。BUILD_LIST
は[]
、CALL_FUNCTION
は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
?いいえ、それはlist
です、list
はタイプPyCFunction
ではありませんPyMethodType
?いいえ、前を参照してください。PyFunctionType
?いいえ、前を参照してください。list
型を呼び出しています。call_function
に渡される引数は PyList_Type
です。 CPythonは、ジェネリック関数を呼び出して _PyObject_FastCallKeywords
という名前の呼び出し可能なオブジェクトを処理する必要があります。
この関数は、特定の関数タイプに対していくつかのチェックを行い(理由はわかりません)、kwargsの辞書を作成した後必要な場合、 _PyObject_FastCallDict
を呼び出します。 。
_PyObject_FastCallDict
が最終的にどこかに到達します!さらにチェックを実行した後、それは 渡したtype
のtype
からtp_call
スロットを取得します。つまり、type.tp_call
を取得します。次に、_PyStack_AsTuple
で渡された引数からTupleを作成し、最後に呼び出しが最終的に行われる!
tp_call
に一致するtype.__call__
が引き継ぎ、最終的にリストオブジェクトを作成します。 __new__
に対応するリストPyType_GenericNew
を呼び出し、 PyType_GenericAlloc
でメモリを割り当てます:これは実際にPyList_New
に追いつく部分です。最後に。オブジェクトを一般的な方法で処理するには、上記のすべてが必要です。
最後に、type_call
はlist.__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()
が呼び出し可能オブジェクトを探してそれを呼び出すことを示しました。
一方、[]
はリスト表示またはリテラルであるため、名前の検索と関数呼び出しは避けます。