回答:
Python 2の場合、オーバーヘッドの多くを取り除くと、実際の速度の差は70%(またはそれ以上)に近づきます。
オブジェクトの作成に問題はありません。1文字の文字列がキャッシュされるため、どちらの方法でも新しいオブジェクトは作成されません。
違いは明白ではありませんが、タイプと整形式に関して、文字列のインデックス付けに関する多数のチェックから作成された可能性があります。また、何を返却するかを確認する必要があるためと考えられます。
リストのインデックス作成は非常に高速です。
>>> python3 -m timeit '[x for x in "abc"]'
1000000 loops, best of 3: 0.388 usec per loop
>>> python3 -m timeit '[x for x in ["a", "b", "c"]]'
1000000 loops, best of 3: 0.436 usec per loop
これはあなたが見つけたものと一致しません...
その場合、Python 2を使用している必要があります。
>>> python2 -m timeit '[x for x in "abc"]'
1000000 loops, best of 3: 0.309 usec per loop
>>> python2 -m timeit '[x for x in ["a", "b", "c"]]'
1000000 loops, best of 3: 0.212 usec per loop
バージョン間の違いを説明しましょう。コンパイルされたコードを調べます。
Python 3の場合:
import dis
def list_iterate():
[item for item in ["a", "b", "c"]]
dis.dis(list_iterate)
#>>> 4 0 LOAD_CONST 1 (<code object <listcomp> at 0x7f4d06b118a0, file "", line 4>)
#>>> 3 LOAD_CONST 2 ('list_iterate.<locals>.<listcomp>')
#>>> 6 MAKE_FUNCTION 0
#>>> 9 LOAD_CONST 3 ('a')
#>>> 12 LOAD_CONST 4 ('b')
#>>> 15 LOAD_CONST 5 ('c')
#>>> 18 BUILD_LIST 3
#>>> 21 GET_ITER
#>>> 22 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
#>>> 25 POP_TOP
#>>> 26 LOAD_CONST 0 (None)
#>>> 29 RETURN_VALUE
def string_iterate():
[item for item in "abc"]
dis.dis(string_iterate)
#>>> 21 0 LOAD_CONST 1 (<code object <listcomp> at 0x7f4d06b17150, file "", line 21>)
#>>> 3 LOAD_CONST 2 ('string_iterate.<locals>.<listcomp>')
#>>> 6 MAKE_FUNCTION 0
#>>> 9 LOAD_CONST 3 ('abc')
#>>> 12 GET_ITER
#>>> 13 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
#>>> 16 POP_TOP
#>>> 17 LOAD_CONST 0 (None)
#>>> 20 RETURN_VALUE
毎回リストが作成されるため、リストのバリアントは遅くなる可能性が高いことがわかります。
これは
9 LOAD_CONST 3 ('a')
12 LOAD_CONST 4 ('b')
15 LOAD_CONST 5 ('c')
18 BUILD_LIST 3
部。文字列バリアントには
9 LOAD_CONST 3 ('abc')
これが違いを生むように見えることを確認できます:
def string_iterate():
[item for item in ("a", "b", "c")]
dis.dis(string_iterate)
#>>> 35 0 LOAD_CONST 1 (<code object <listcomp> at 0x7f4d068be660, file "", line 35>)
#>>> 3 LOAD_CONST 2 ('string_iterate.<locals>.<listcomp>')
#>>> 6 MAKE_FUNCTION 0
#>>> 9 LOAD_CONST 6 (('a', 'b', 'c'))
#>>> 12 GET_ITER
#>>> 13 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
#>>> 16 POP_TOP
#>>> 17 LOAD_CONST 0 (None)
#>>> 20 RETURN_VALUE
これはちょうど生成します
9 LOAD_CONST 6 (('a', 'b', 'c'))
タプルは不変なので。テスト:
>>> python3 -m timeit '[x for x in ("a", "b", "c")]'
1000000 loops, best of 3: 0.369 usec per loop
すばらしい、スピードアップ。
Python 2の場合:
def list_iterate():
[item for item in ["a", "b", "c"]]
dis.dis(list_iterate)
#>>> 2 0 BUILD_LIST 0
#>>> 3 LOAD_CONST 1 ('a')
#>>> 6 LOAD_CONST 2 ('b')
#>>> 9 LOAD_CONST 3 ('c')
#>>> 12 BUILD_LIST 3
#>>> 15 GET_ITER
#>>> >> 16 FOR_ITER 12 (to 31)
#>>> 19 STORE_FAST 0 (item)
#>>> 22 LOAD_FAST 0 (item)
#>>> 25 LIST_APPEND 2
#>>> 28 JUMP_ABSOLUTE 16
#>>> >> 31 POP_TOP
#>>> 32 LOAD_CONST 0 (None)
#>>> 35 RETURN_VALUE
def string_iterate():
[item for item in "abc"]
dis.dis(string_iterate)
#>>> 2 0 BUILD_LIST 0
#>>> 3 LOAD_CONST 1 ('abc')
#>>> 6 GET_ITER
#>>> >> 7 FOR_ITER 12 (to 22)
#>>> 10 STORE_FAST 0 (item)
#>>> 13 LOAD_FAST 0 (item)
#>>> 16 LIST_APPEND 2
#>>> 19 JUMP_ABSOLUTE 7
#>>> >> 22 POP_TOP
#>>> 23 LOAD_CONST 0 (None)
#>>> 26 RETURN_VALUE
奇妙なことに、リストの構築は同じですが、この方が高速です。Python 2は奇妙に速く動作しています。
内包表記を削除して、リタイムします。_ =
それはアウトに最適化取得を防止することです。
>>> python3 -m timeit '_ = ["a", "b", "c"]'
10000000 loops, best of 3: 0.0707 usec per loop
>>> python3 -m timeit '_ = "abc"'
100000000 loops, best of 3: 0.0171 usec per loop
初期化はバージョン間の違いを説明するのに十分重要ではないことがわかります(これらの数は小さいです)。したがって、Python 3の理解は遅いと結論付けることができます。これは、Python 3がより安全なスコープを持つように理解を変更したため、理にかなっています。
さて、今度はベンチマークを改善します(反復ではないオーバーヘッドを削除しているだけです)。これにより、事前割り当てによってイテラブルの構築が削除されます。
>>> python3 -m timeit -s 'iterable = "abc"' '[x for x in iterable]'
1000000 loops, best of 3: 0.387 usec per loop
>>> python3 -m timeit -s 'iterable = ["a", "b", "c"]' '[x for x in iterable]'
1000000 loops, best of 3: 0.368 usec per loop
>>> python2 -m timeit -s 'iterable = "abc"' '[x for x in iterable]'
1000000 loops, best of 3: 0.309 usec per loop
>>> python2 -m timeit -s 'iterable = ["a", "b", "c"]' '[x for x in iterable]'
10000000 loops, best of 3: 0.164 usec per loop
呼び出しiter
がオーバーヘッドであるかどうかを確認できます。
>>> python3 -m timeit -s 'iterable = "abc"' 'iter(iterable)'
10000000 loops, best of 3: 0.099 usec per loop
>>> python3 -m timeit -s 'iterable = ["a", "b", "c"]' 'iter(iterable)'
10000000 loops, best of 3: 0.1 usec per loop
>>> python2 -m timeit -s 'iterable = "abc"' 'iter(iterable)'
10000000 loops, best of 3: 0.0913 usec per loop
>>> python2 -m timeit -s 'iterable = ["a", "b", "c"]' 'iter(iterable)'
10000000 loops, best of 3: 0.0854 usec per loop
いいえ、違います。特にPython 3の場合、違いは小さすぎます。
それで、不要なオーバーヘッドをさらに削除しましょう...全体を遅くします!目的は、反復を長くして、時間がオーバーヘッドを隠すことです。
>>> python3 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' '[x for x in iterable]'
100 loops, best of 3: 3.12 msec per loop
>>> python3 -m timeit -s 'import random; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' '[x for x in iterable]'
100 loops, best of 3: 2.77 msec per loop
>>> python2 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' '[x for x in iterable]'
100 loops, best of 3: 2.32 msec per loop
>>> python2 -m timeit -s 'import random; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' '[x for x in iterable]'
100 loops, best of 3: 2.09 msec per loop
これは実際にはそれほど変わっていませんが、少しは役に立ちました。
だから、理解を取り除く。質問の一部ではないオーバーヘッドです:
>>> python3 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'for x in iterable: pass'
1000 loops, best of 3: 1.71 msec per loop
>>> python3 -m timeit -s 'import random; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' 'for x in iterable: pass'
1000 loops, best of 3: 1.36 msec per loop
>>> python2 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'for x in iterable: pass'
1000 loops, best of 3: 1.27 msec per loop
>>> python2 -m timeit -s 'import random; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' 'for x in iterable: pass'
1000 loops, best of 3: 935 usec per loop
それはもっとそれに似ています!を使用deque
して反復することで、わずかに速くなります。基本的には同じですが、高速です:
>>> python3 -m timeit -s 'import random; from collections import deque; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 777 usec per loop
>>> python3 -m timeit -s 'import random; from collections import deque; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 405 usec per loop
>>> python2 -m timeit -s 'import random; from collections import deque; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 805 usec per loop
>>> python2 -m timeit -s 'import random; from collections import deque; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 438 usec per loop
印象的なのは、Unicodeがバイト文字列と競合できることです。私たちは、明示的に試みることによってこれを確認することができますbytes
し、unicode
両方に:
bytes
>>> python3 -m timeit -s 'import random; from collections import deque; iterable = b"".join(chr(random.randint(0, 127)).encode("ascii") for _ in range(100000))' 'deque(iterable, maxlen=0)' :(
1000 loops, best of 3: 571 usec per loop
>>> python3 -m timeit -s 'import random; from collections import deque; iterable = [chr(random.randint(0, 127)).encode("ascii") for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 394 usec per loop
>>> python2 -m timeit -s 'import random; from collections import deque; iterable = b"".join(chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 757 usec per loop
>>> python2 -m timeit -s 'import random; from collections import deque; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 438 usec per loop
ここでは、Python 3が実際にはPython 2 よりも高速であることがわかります。
unicode
>>> python3 -m timeit -s 'import random; from collections import deque; iterable = u"".join( chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 800 usec per loop
>>> python3 -m timeit -s 'import random; from collections import deque; iterable = [ chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 394 usec per loop
>>> python2 -m timeit -s 'import random; from collections import deque; iterable = u"".join(unichr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 1.07 msec per loop
>>> python2 -m timeit -s 'import random; from collections import deque; iterable = [unichr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 469 usec per loop
繰り返しになりますが、これは予想されますが、Python 3の方が高速です。str
Python 3では多くの注意が払われました)。
実際、これは unicode
- bytes
差は非常に小さく、印象的です。
それでは、この1つのケースを分析してみましょう。私にとっては速くて便利です。
>>> python3 -m timeit -s 'import random; from collections import deque; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 777 usec per loop
>>> python3 -m timeit -s 'import random; from collections import deque; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 405 usec per loop
私たちは実際にティム・ピーターの10倍の賛成回答を除外できます!
>>> foo = iterable[123]
>>> iterable[36] is foo
True
しかし、これは言及する価値があります:インデックス作成コスト。違いはインデックス付けにある可能性が高いので、反復を削除してインデックスを付けます。
>>> python3 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'iterable[123]'
10000000 loops, best of 3: 0.0397 usec per loop
>>> python3 -m timeit -s 'import random; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' 'iterable[123]'
10000000 loops, best of 3: 0.0374 usec per loop
違いは小さいように見えますが、コストの少なくとも半分はオーバーヘッドです。
>>> python3 -m timeit -s 'import random; iterable = [chr(random.randint(0, 127)) for _ in range(100000)]' 'iterable; 123'
100000000 loops, best of 3: 0.0173 usec per loop
速度の違いはそれを非難することを決定するのに十分です。おもう。
それでは、なぜリストのインデックス作成がこれほど高速になるのでしょうか。
さて、それについては後で説明しますが、おそらく、インターンされた文字列(または、個別のメカニズムの場合はキャッシュされた文字)をチェックすることです。これは最適な速度よりも遅くなります。しかし、私はソースをチェックしに行きます(私はCで快適ではありませんが...):)。
だからここにソースがあります:
static PyObject *
unicode_getitem(PyObject *self, Py_ssize_t index)
{
void *data;
enum PyUnicode_Kind kind;
Py_UCS4 ch;
PyObject *res;
if (!PyUnicode_Check(self) || PyUnicode_READY(self) == -1) {
PyErr_BadArgument();
return NULL;
}
if (index < 0 || index >= PyUnicode_GET_LENGTH(self)) {
PyErr_SetString(PyExc_IndexError, "string index out of range");
return NULL;
}
kind = PyUnicode_KIND(self);
data = PyUnicode_DATA(self);
ch = PyUnicode_READ(kind, data, index);
if (ch < 256)
return get_latin1_char(ch);
res = PyUnicode_New(1, ch);
if (res == NULL)
return NULL;
kind = PyUnicode_KIND(res);
data = PyUnicode_DATA(res);
PyUnicode_WRITE(kind, data, 0, ch);
assert(_PyUnicode_CheckConsistency(res, 1));
return res;
}
上から歩いて、いくつかのチェックがあります。これらは退屈です。次に、いくつかの割り当てがありますが、これも退屈です。最初の興味深い行は
ch = PyUnicode_READ(kind, data, index);
しかし、インデックスを付けることで隣接するC配列から読み取るので、それが高速であることを願っています。結果ch
は256未満になるため、キャッシュされた文字をで返しますget_latin1_char(ch)
。
実行します(最初のチェックを削除します)
kind = PyUnicode_KIND(self);
data = PyUnicode_DATA(self);
ch = PyUnicode_READ(kind, data, index);
return get_latin1_char(ch);
どこ
#define PyUnicode_KIND(op) \
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject *)(op))->state.kind)
(これはアサートがデバッグで無視されるため(退屈なので、高速であることを確認でき((PyASCIIObject *)(op))->state.kind)
ます)、間接参照であり、Cレベルのキャストです(私はそう思います)。
#define PyUnicode_DATA(op) \
(assert(PyUnicode_Check(op)), \
PyUnicode_IS_COMPACT(op) ? _PyUnicode_COMPACT_DATA(op) : \
_PyUnicode_NONCOMPACT_DATA(op))
(これも同様の理由で退屈です。マクロ(Something_CAPITALIZED
)がすべて高速であると想定しています)、
#define PyUnicode_READ(kind, data, index) \
((Py_UCS4) \
((kind) == PyUnicode_1BYTE_KIND ? \
((const Py_UCS1 *)(data))[(index)] : \
((kind) == PyUnicode_2BYTE_KIND ? \
((const Py_UCS2 *)(data))[(index)] : \
((const Py_UCS4 *)(data))[(index)] \
) \
))
(これにはインデックスが含まれますが、実際にはまったく遅くありません)
static PyObject*
get_latin1_char(unsigned char ch)
{
PyObject *unicode = unicode_latin1[ch];
if (!unicode) {
unicode = PyUnicode_New(1, ch);
if (!unicode)
return NULL;
PyUnicode_1BYTE_DATA(unicode)[0] = ch;
assert(_PyUnicode_CheckConsistency(unicode, 1));
unicode_latin1[ch] = unicode;
}
Py_INCREF(unicode);
return unicode;
}
それは私の疑いを確認します:
これはキャッシュされます:
PyObject *unicode = unicode_latin1[ch];
これは速いはずです。if (!unicode)
それはこの場合、文字通り同等ですので、実行されません
PyObject *unicode = unicode_latin1[ch];
Py_INCREF(unicode);
return unicode;
正直なところ、assert
sをテストした後(それらを無効にすることで[ Cレベルのアサートで機能すると思います ...])、もっとも遅いのは次の部分だけです。
PyUnicode_IS_COMPACT(op)
_PyUnicode_COMPACT_DATA(op)
_PyUnicode_NONCOMPACT_DATA(op)
どれが:
#define PyUnicode_IS_COMPACT(op) \
(((PyASCIIObject*)(op))->state.compact)
(以前と同じように高速)、
#define _PyUnicode_COMPACT_DATA(op) \
(PyUnicode_IS_ASCII(op) ? \
((void*)((PyASCIIObject*)(op) + 1)) : \
((void*)((PyCompactUnicodeObject*)(op) + 1)))
(マクロIS_ASCII
が高速の場合は高速)、および
#define _PyUnicode_NONCOMPACT_DATA(op) \
(assert(((PyUnicodeObject*)(op))->data.any), \
((((PyUnicodeObject *)(op))->data.any)))
(これもアサートと間接とキャストの高速なので)。
だから私たちは(うさぎの穴)にダウンしています:
PyUnicode_IS_ASCII
それは
#define PyUnicode_IS_ASCII(op) \
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject*)op)->state.ascii)
うーん...それはあまりにも速いようです...
さて、わかりましたが、それをと比較してみましょうPyList_GetItem
。(うん、ティム・ピーターズにもっと仕事をしてくれてありがとう:P)
PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
if (!PyList_Check(op)) {
PyErr_BadInternalCall();
return NULL;
}
if (i < 0 || i >= Py_SIZE(op)) {
if (indexerr == NULL) {
indexerr = PyUnicode_FromString(
"list index out of range");
if (indexerr == NULL)
return NULL;
}
PyErr_SetObject(PyExc_IndexError, indexerr);
return NULL;
}
return ((PyListObject *)op) -> ob_item[i];
}
エラー以外のケースでは、これが実行されるだけであることがわかります。
PyList_Check(op)
Py_SIZE(op)
((PyListObject *)op) -> ob_item[i]
どこPyList_Check
に
#define PyList_Check(op) \
PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)
(TABS ! TABS !!!)(issue21587)5分で修正され、マージされました。ええと...ええ。くそー。彼らはスキートを恥にさらした。
#define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size)
#define PyType_FastSubclass(t,f) PyType_HasFeature(t,f)
#ifdef Py_LIMITED_API
#define PyType_HasFeature(t,f) ((PyType_GetFlags(t) & (f)) != 0)
#else
#define PyType_HasFeature(t,f) (((t)->tp_flags & (f)) != 0)
#endif
したがって、これがPy_LIMITED_API
オンでない限り、これは通常は非常に簡単です(2つの間接参照といくつかのブール値チェック)。その場合、... ???
次に、インデックス付けとキャスト(((PyListObject *)op) -> ob_item[i]
)があれば完了です。
そのため、リストのチェックは間違いなく少なく、速度のわずかな違いは確かに関連性があることを意味します。
一般に、(->)
Unicodeには型チェックと間接参照があるだけだと思います。ポイントが足りないようですが、どうですか?
get_latin1_char()
トリックはには存在せずunicode_getitem()
、下位レベルに存在しますunicode_char
。そのため、現在、別のレベルの関数呼び出しがあります-使用しないかどうか(使用するコンパイラーおよび最適化フラグによって異なります)。この詳細レベルでは、信頼できる回答はありません;-)
ほとんどのコンテナーオブジェクト(リスト、タプル、辞書など)を反復処理すると、イテレーターがコンテナー内のオブジェクトを配信します。
ただし、文字列を反復処理する場合、配信される文字ごとに新しいオブジェクトを作成する必要があります。文字列はリストと同じ意味での「コンテナ」ではありません。文字列内の個々の文字は、反復によってこれらのオブジェクトが作成されるまで、個別のオブジェクトとして存在しません。
is
ます。それは正しいように聞こえますが、私は実際にそうであるとは思いません。
stringobject.c
は、__getitem__
文字列の場合、格納されている1文字の文字列のテーブルから結果を取得するだけなので、それらの割り当てコストは1回しか発生しないことを示しています。
s = chr(256)
がs is chr(256)
返されますFalse
-データ値をトリガーするカバーの下に特別なケースの山が存在するため、タイプを知るだけでは十分ではありません。
文字列のイテレータを作成すると、オーバーヘッドが発生する可能性があります。一方、インスタンス化時には、配列にはすでにイテレータが含まれています。
編集:
>>> timeit("[x for x in ['a','b','c']]")
0.3818681240081787
>>> timeit("[x for x in 'abc']")
0.3732869625091553
これは2.7を使用して実行されましたが、私のMacブックプロi7では。これは、システム構成の違いの結果である可能性があります。