TL; DR
Python 2 için, ek yükün çoğu kaldırıldığında gerçek hız farkı% 70'e (veya daha fazlasına) yakındır.
Nesne oluşturma hatalı değildir . Tek karakterli dizeler önbelleğe alındığı için hiçbir yöntem yeni bir nesne oluşturmaz.
Aradaki fark açık değildir, ancak büyük olasılıkla tür ve iyi biçim açısından dizi indekslemede daha fazla sayıda denetimden kaynaklanmaktadır. Neyin iade edileceğini kontrol etme ihtiyacı da oldukça muhtemeldir.
Liste indeksleme oldukça hızlıdır.
>>> 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
Bu bulduğunuz şeyle uyuşmuyor ...
O halde Python 2 kullanıyor olmalısın.
>>> 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
Sürümler arasındaki farkı açıklayalım. Derlenmiş kodu inceleyeceğim.
Python 3 için:
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
Burada, her seferinde listenin oluşturulması nedeniyle liste varyantının daha yavaş olacağını görüyorsunuz.
Bu
9 LOAD_CONST 3 ('a')
12 LOAD_CONST 4 ('b')
15 LOAD_CONST 5 ('c')
18 BUILD_LIST 3
Bölüm. Dize varyantında yalnızca
9 LOAD_CONST 3 ('abc')
Bunun bir fark yarattığını kontrol edebilirsiniz:
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
Bu sadece üretir
9 LOAD_CONST 6 (('a', 'b', 'c'))
tuplelar değişmez olduğu için. Ölçek:
>>> python3 -m timeit '[x for x in ("a", "b", "c")]'
1000000 loops, best of 3: 0.369 usec per loop
Harika, hızınızı artırın.
Python 2 için:
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
Garip olan şey, listenin aynı yapısına sahip olmamız , ancak bunun için hala daha hızlı. Python 2 garip bir şekilde hızlı davranıyor.
Anlamaları ve yeniden zamanı kaldıralım. Bu _ =
, optimize edilmesini önlemektir.
>>> 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
Başlatma işleminin sürümler arasındaki farkı hesaba katacak kadar önemli olmadığını görebiliriz (bu sayılar küçüktür)! Böylece Python 3'ün daha yavaş kavrayışa sahip olduğu sonucuna varabiliriz. Python 3, daha güvenli kapsama sahip olmak için anlamaları değiştirdiği için bu mantıklıdır.
Şimdi kıyaslamayı iyileştirin (sadece yineleme olmayan genel giderleri kaldırıyorum). Bu, yinelenebilirin yapısını önceden atayarak kaldırır:
>>> 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
Aramanın ek yük olup olmadığını kontrol edebiliriz :
>>> 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
Hayır. Hayır değil. Fark, özellikle Python 3 için çok küçük.
Öyleyse, her şeyi yavaşlatarak daha fazla istenmeyen yükü kaldıralım! Amaç sadece daha uzun bir yinelemeye sahip olmak, böylece zamanın üstte gizlenmesi.
>>> 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
Bu aslında pek değişmedi ama biraz yardımı oldu.
Öyleyse anlayışı kaldırın. Sorunun bir parçası olmayan genel giderler:
>>> 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
Bu daha doğru gibi! deque
Yinelemeyi kullanarak biraz daha hızlı olabiliriz . Temelde aynı, ancak daha hızlı :
>>> 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
Beni etkileyen şey, Unicode'un bytestrings ile rekabet edebilmesi. Bunu deneyerek bytes
ve unicode
her ikisinde de açıkça kontrol edebiliriz :
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
Burada Python 3'ü aslında Python 2'den daha hızlı görüyorsunuz .
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
Yine, Python 3 daha hızlıdır, ancak bu beklenmelidir ( str
Python 3'te çok dikkat çekmiştir).
Aslında, bu unicode
- bytes
fark çok küçük ve bu etkileyici.
Öyleyse, benim için hızlı ve kullanışlı olduğunu görerek bu durumu analiz edelim:
>>> 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
Tim Peter'ın 10 kez olumlu oy verilen cevabını gerçekten göz ardı edebiliriz!
>>> foo = iterable[123]
>>> iterable[36] is foo
True
Bunlar yeni nesneler değil!
Ancak şunu belirtmeye değer: endeksleme maliyetleri . Fark büyük olasılıkla dizine eklemede olacaktır, bu nedenle yinelemeyi kaldırın ve yalnızca dizine ekleyin:
>>> 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
Fark küçük görünüyor, ancak maliyetin en az yarısı genel giderler:
>>> 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
bu yüzden hız farkı onu suçlamaya karar vermek için yeterlidir. Bence.
Öyleyse neden bir listeyi indekslemek çok daha hızlı?
Pekala, size bu konuda geri döneceğim, ancak benim tahminim bu, dahili dizeleri (veya ayrı bir mekanizma ise önbelleğe alınmış karakterleri) kontrol etmeye bağlı. Bu, optimumdan daha az hızlı olacaktır. Ama kaynağa bakacağım (C ... konusunda rahat olmasam da) :).
İşte kaynak:
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;
}
Yukarıdan yürürken, bazı kontrollerimiz olacak. Bunlar sıkıcı. Sonra da sıkıcı olması gereken bazı ödevler. İlk ilginç satır
ch = PyUnicode_READ(kind, data, index);
ancak bitişik bir C dizisini indeksleyerek okuduğumuz için bunun hızlı olmasını umuyoruz . Sonuç, ch
256'dan az olacak, bu nedenle önbelleğe alınan karakteri içinde döndüreceğiz get_latin1_char(ch)
.
Öyleyse koşacağız (ilk kontrolleri bırakacağız)
kind = PyUnicode_KIND(self);
data = PyUnicode_DATA(self);
ch = PyUnicode_READ(kind, data, index);
return get_latin1_char(ch);
Nerede
#define PyUnicode_KIND(op) \
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject *)(op))->state.kind)
(ki bu sıkıcı çünkü iddialar hata ayıklamada göz ardı ediliyor [böylece hızlı olduklarını kontrol edebilirim] ve ((PyASCIIObject *)(op))->state.kind)
(bence) bir dolaylı ve C-seviyesi döküm);
#define PyUnicode_DATA(op) \
(assert(PyUnicode_Check(op)), \
PyUnicode_IS_COMPACT(op) ? _PyUnicode_COMPACT_DATA(op) : \
_PyUnicode_NONCOMPACT_DATA(op))
(makroların ( Something_CAPITALIZED
) hepsinin hızlı olduğunu varsayarak, benzer nedenlerden dolayı da sıkıcıdır ),
#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)] \
) \
))
(dizinler içerir ancak gerçekten yavaş değildir) ve
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;
}
Bu şüphelerimi doğruluyor:
Bu önbelleğe alınır:
PyObject *unicode = unicode_latin1[ch];
Bu hızlı olmalı. if (!unicode)
O kadar bu durumda tam anlamıyla eşdeğer yüzden, koşmak değildir
PyObject *unicode = unicode_latin1[ch];
Py_INCREF(unicode);
return unicode;
Dürüst olmak gerekirse, s'lerin assert
hızlı olduğunu test ettikten sonra (onları devre dışı bırakarak [ C düzeyindeki iddialarda çalıştığını düşünüyorum ...]), makul derecede yavaş olan tek kısımlar:
PyUnicode_IS_COMPACT(op)
_PyUnicode_COMPACT_DATA(op)
_PyUnicode_NONCOMPACT_DATA(op)
Hangileri:
#define PyUnicode_IS_COMPACT(op) \
(((PyASCIIObject*)(op))->state.compact)
(eskisi gibi hızlı),
#define _PyUnicode_COMPACT_DATA(op) \
(PyUnicode_IS_ASCII(op) ? \
((void*)((PyASCIIObject*)(op) + 1)) : \
((void*)((PyCompactUnicodeObject*)(op) + 1)))
(makro IS_ASCII
hızlıysa hızlı) ve
#define _PyUnicode_NONCOMPACT_DATA(op) \
(assert(((PyUnicodeObject*)(op))->data.any), \
((((PyUnicodeObject *)(op))->data.any)))
(aynı zamanda bir iddia artı bir dolaylama artı bir döküm olduğu için hızlı).
Öyleyse aşağıdayız (tavşan deliğinden):
PyUnicode_IS_ASCII
hangisi
#define PyUnicode_IS_ASCII(op) \
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject*)op)->state.ascii)
Hmm ... bu da hızlı görünüyor ...
Peki, tamam, ama bunu karşılaştıralım PyList_GetItem
. (Evet, Tim Peters'a yapmam için daha çok iş verdiği için teşekkürler : S.)
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];
}
Hata olmayan durumlarda bunun sadece çalışacağını görebiliriz:
PyList_Check(op)
Py_SIZE(op)
((PyListObject *)op) -> ob_item[i]
nerede PyList_Check
olduğunu
#define PyList_Check(op) \
PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)
( SEKMELER ! SEKMELER !!! ) ( sorun21587 ) Bu 5 dakika içinde düzeltildi ve birleştirildi . Mesela ... evet. Lanet olsun. Skeet'i utandırdılar.
#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
Yani bu normalde gerçekten önemsizdir (iki yönlendirme ve birkaç boole kontrolü), eğer Py_LIMITED_API
açık değilse , bu durumda ... ???
Sonra indeksleme ve bir oyuncu ( ((PyListObject *)op) -> ob_item[i]
) var ve işimiz bitti.
Bu yüzden listeler için kesinlikle daha az kontrol vardır ve küçük hız farkları kesinlikle bunun alakalı olabileceğini ima eder.
Bence genel olarak (->)
, Unicode için yalnızca daha fazla tür denetimi ve indirme var. Görünüşe göre bir noktayı kaçırıyorum, ama ne ?