TL; DR
Perbedaan kecepatan aktual lebih dekat ke 70% (atau lebih) begitu banyak overhead dihapus, untuk Python 2.
Penciptaan objek tidak bersalah. Tidak ada metode yang menciptakan objek baru, karena string satu karakter di-cache.
Perbedaannya tidak jelas, tetapi kemungkinan dibuat dari sejumlah besar pemeriksaan pada pengindeksan string, berkaitan dengan jenis dan pembentukan yang baik. Ini juga kemungkinan besar berkat kebutuhan untuk memeriksa apa yang harus dikembalikan.
Daftar indeks sangat cepat.
>>> 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
Ini tidak setuju dengan apa yang Anda temukan ...
Anda harus menggunakan Python 2, lalu.
>>> 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
Mari kita jelaskan perbedaan antar versi. Saya akan memeriksa kode yang dikompilasi.
Untuk 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
Anda lihat di sini bahwa varian daftar cenderung lebih lambat karena pembangunan daftar setiap kali.
Ini adalah
9 LOAD_CONST 3 ('a')
12 LOAD_CONST 4 ('b')
15 LOAD_CONST 5 ('c')
18 BUILD_LIST 3
bagian. Varian string hanya memiliki
9 LOAD_CONST 3 ('abc')
Anda dapat memeriksa apakah ini tampaknya membuat perbedaan:
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
Ini menghasilkan adil
9 LOAD_CONST 6 (('a', 'b', 'c'))
sebagai tuple tidak berubah. Uji:
>>> python3 -m timeit '[x for x in ("a", "b", "c")]'
1000000 loops, best of 3: 0.369 usec per loop
Bagus, kembali ke kecepatan.
Untuk 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
Yang aneh adalah kami memiliki daftar yang sama , tetapi masih lebih cepat untuk ini. Python 2 bertingkah anehnya cepat.
Mari kita hilangkan pemahaman dan waktu ulang. Ini _ =
untuk mencegahnya dioptimalkan.
>>> 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
Kita dapat melihat bahwa inisialisasi tidak cukup signifikan untuk menjelaskan perbedaan antara versi (angka-angka itu kecil)! Dengan demikian kita dapat menyimpulkan bahwa Python 3 memiliki pemahaman yang lebih lambat. Ini masuk akal karena Python 3 mengubah pemahaman untuk memiliki pelingkupan yang lebih aman.
Nah, sekarang perbaiki patokannya (saya hanya menghapus overhead yang bukan iterasi). Ini menghapus bangunan iterable dengan pra-menugaskannya:
>>> 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
Kami dapat memeriksa apakah panggilan iter
adalah biaya overhead:
>>> 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
Tidak, tidak. Perbedaannya terlalu kecil, terutama untuk Python 3.
Jadi mari kita hilangkan overhead yang tidak diinginkan ... dengan membuat semuanya lebih lambat! Tujuannya hanya untuk memiliki iterasi yang lebih lama sehingga waktu bersembunyi di atas kepala.
>>> 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
Ini sebenarnya tidak banyak berubah , tetapi sedikit membantu.
Jadi hilangkan pemahamannya. Ada overhead yang bukan bagian dari pertanyaan:
>>> 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
Itu lebih seperti itu! Kita bisa mendapatkan sedikit lebih cepat dengan menggunakan deque
iterate. Pada dasarnya sama, tetapi lebih cepat :
>>> 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
Yang mengesankan bagi saya adalah bahwa Unicode bersaing dengan bytestrings. Kami dapat memeriksa ini secara eksplisit dengan mencoba bytes
dan unicode
keduanya:
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
Di sini Anda melihat Python 3 sebenarnya lebih cepat dari 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
Sekali lagi, Python 3 lebih cepat, meskipun ini yang diharapkan ( str
telah memiliki banyak perhatian di Python 3).
Sebenarnya, ini unicode
- bytes
perbedaannya sangat kecil, yang mengesankan.
Jadi mari kita menganalisis satu kasus ini, mengingat cepat dan nyaman bagi saya:
>>> 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
Kami benar-benar dapat mengesampingkan jawaban Tim Peter yang 10 kali lipat!
>>> foo = iterable[123]
>>> iterable[36] is foo
True
Ini bukan benda baru!
Tapi ini layak disebutkan: biaya pengindeksan . Perbedaannya kemungkinan terletak pada pengindeksan, jadi hapus iterasi dan indeks saja:
>>> 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
Perbedaannya tampak kecil, tetapi setidaknya setengah dari biaya overhead:
>>> 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
jadi perbedaan kecepatan cukup untuk memutuskan untuk menyalahkannya. Kupikir.
Jadi mengapa pengindeksan daftar jauh lebih cepat?
Yah, saya akan kembali kepada Anda tentang hal itu, tetapi dugaan saya adalah itu tergantung pada cek untuk string yang diinternir (atau karakter yang di-cache jika ini merupakan mekanisme yang terpisah). Ini akan kurang cepat dari optimal. Tapi saya akan memeriksa sumbernya (walaupun saya tidak nyaman di C ...) :).
Jadi, inilah sumbernya:
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;
}
Berjalan dari atas, kami akan periksa. Ini membosankan. Kemudian beberapa tugas, yang juga harus membosankan. Baris pertama yang menarik adalah
ch = PyUnicode_READ(kind, data, index);
tapi kami berharap itu cepat, karena kami membaca dari array C yang berdekatan dengan mengindeksnya. Hasilnya,, ch
akan kurang dari 256 sehingga kami akan mengembalikan karakter yang di-cache get_latin1_char(ch)
.
Jadi kita akan lari (menjatuhkan cek pertama)
kind = PyUnicode_KIND(self);
data = PyUnicode_DATA(self);
ch = PyUnicode_READ(kind, data, index);
return get_latin1_char(ch);
Dimana
#define PyUnicode_KIND(op) \
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject *)(op))->state.kind)
(Yang membosankan karena menegaskan diabaikan dalam debug [sehingga saya dapat memeriksa bahwa mereka cepat] dan ((PyASCIIObject *)(op))->state.kind)
(saya pikir) adalah tipuan dan pemain tingkat C);
#define PyUnicode_DATA(op) \
(assert(PyUnicode_Check(op)), \
PyUnicode_IS_COMPACT(op) ? _PyUnicode_COMPACT_DATA(op) : \
_PyUnicode_NONCOMPACT_DATA(op))
(Yang juga membosankan karena alasan yang sama, dengan asumsi makro ( Something_CAPITALIZED
) semuanya cepat),
#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)] \
) \
))
(yang melibatkan indeks tetapi sebenarnya tidak lambat sama sekali) dan
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;
}
Yang menegaskan kecurigaan saya bahwa:
Ini di-cache:
PyObject *unicode = unicode_latin1[ch];
Ini harus cepat. Ini if (!unicode)
tidak dijalankan, jadi secara harfiah setara dalam hal ini
PyObject *unicode = unicode_latin1[ch];
Py_INCREF(unicode);
return unicode;
Jujur, setelah menguji assert
s cepat (dengan menonaktifkannya [saya pikir itu bekerja pada C-level menegaskan ...]), satu-satunya bagian yang masuk akal-lambat adalah:
PyUnicode_IS_COMPACT(op)
_PyUnicode_COMPACT_DATA(op)
_PyUnicode_NONCOMPACT_DATA(op)
Yang mana:
#define PyUnicode_IS_COMPACT(op) \
(((PyASCIIObject*)(op))->state.compact)
(cepat, seperti sebelumnya),
#define _PyUnicode_COMPACT_DATA(op) \
(PyUnicode_IS_ASCII(op) ? \
((void*)((PyASCIIObject*)(op) + 1)) : \
((void*)((PyCompactUnicodeObject*)(op) + 1)))
(cepat jika makro IS_ASCII
cepat), dan
#define _PyUnicode_NONCOMPACT_DATA(op) \
(assert(((PyUnicodeObject*)(op))->data.any), \
((((PyUnicodeObject *)(op))->data.any)))
(Juga secepat itu menegaskan ditambah tipuan ditambah pemain).
Jadi kita turun (lubang kelinci) ke:
PyUnicode_IS_ASCII
yang mana
#define PyUnicode_IS_ASCII(op) \
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject*)op)->state.ascii)
Hmm ... itu sepertinya cepat juga ...
Baiklah, tapi mari kita bandingkan PyList_GetItem
. (Ya, terima kasih Tim Peters karena memberi saya lebih banyak pekerjaan untuk dilakukan: 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];
}
Kita dapat melihat bahwa pada kasus non-kesalahan ini hanya akan berjalan:
PyList_Check(op)
Py_SIZE(op)
((PyListObject *)op) -> ob_item[i]
dimana PyList_Check
adalah
#define PyList_Check(op) \
PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)
( TABS! TABS !!! ) ( issue21587 ) Itu diperbaiki dan digabung dalam 5 menit . Seperti ... ya. Sial. Mereka mempermalukan Skeet.
#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
Jadi ini biasanya sangat sepele (dua tipuan dan beberapa cek boolean) kecuali Py_LIMITED_API
aktif, dalam hal ini ... ???
Lalu ada pengindeksan dan pemeran ( ((PyListObject *)op) -> ob_item[i]
) dan kami selesai.
Jadi pasti ada lebih sedikit pemeriksaan untuk daftar, dan perbedaan kecepatan kecil tentu menyiratkan bahwa itu mungkin relevan.
Saya pikir secara umum, hanya ada lebih banyak pengecekan tipe dan tipuan (->)
untuk Unicode. Sepertinya saya kehilangan satu poin, tapi apa ?