TL; ดร
ความแตกต่างของความเร็วที่แท้จริงนั้นใกล้เคียงกับ 70% (หรือมากกว่า) เมื่อมีการลบค่าโสหุ้ยจำนวนมากออกไปสำหรับ Python 2
การสร้างวัตถุไม่ใช่ความผิด ทั้งสองวิธีจะไม่สร้างวัตถุใหม่เนื่องจากมีการแคชสตริงอักขระหนึ่งตัว
ความแตกต่างไม่ชัดเจน แต่น่าจะสร้างขึ้นจากการตรวจสอบการจัดทำดัชนีสตริงจำนวนมากขึ้นโดยคำนึงถึงประเภทและรูปแบบที่ดี นอกจากนี้ยังค่อนข้างต้องขอบคุณที่ต้องตรวจสอบว่าจะคืนอะไร
การจัดทำดัชนีรายการทำได้เร็วมาก
>>> 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)
def string_iterate():
[item for item in "abc"]
dis.dis(string_iterate)
คุณจะเห็นว่าตัวแปรรายการมีแนวโน้มที่จะช้าลงเนื่องจากการสร้างรายการในแต่ละครั้ง
นี้เป็น
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)
เพียงแค่นี้
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)
def string_iterate():
[item for item in "abc"]
dis.dis(string_iterate)
สิ่งที่แปลกคือเรามีสิ่งปลูกสร้างเดียวกันของรายการ แต่ก็ยังเร็วกว่าสำหรับสิ่งนี้ 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 สามารถแข่งขันกับ bytestrings ได้ เราสามารถตรวจสอบสิ่งนี้อย่างชัดเจนโดยลอง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
ความแตกต่างมีน้อยมากซึ่งน่าประทับใจ
ลองมาวิเคราะห์กันดูว่ามันรวดเร็วและสะดวกสำหรับฉัน:
>>> 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
ดังนั้นความแตกต่างของความเร็วจึงเพียงพอที่จะตัดสินโทษมัน ฉันคิด.
เหตุใดการจัดทำดัชนีรายการจึงเร็วกว่ามาก?
ดีฉันจะกลับมาที่คุณว่า แต่ฉันเดาว่าจะลงไปตรวจสอบสำหรับinternedสตริง (หรือตัวอักษรที่เก็บไว้ชั่วคราวถ้าเป็นกลไกที่แยกต่างหาก) ซึ่งจะเร็วน้อยกว่าที่เหมาะสม แต่ฉันจะไปตรวจสอบแหล่งที่มา (แม้ว่าฉันจะไม่สะดวกใน 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);
ที่ไหน
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject *)(op))->state.kind)
(ซึ่งน่าเบื่อเพราะการยืนยันถูกเพิกเฉยในการดีบั๊ก [ดังนั้นฉันจึงตรวจสอบได้ว่ามันเร็ว] และ((PyASCIIObject *)(op))->state.kind)
(ฉันคิดว่า) เป็นทิศทางและการร่ายระดับ C);
(assert(PyUnicode_Check(op)), \
PyUnicode_IS_COMPACT(op) ? _PyUnicode_COMPACT_DATA(op) : \
_PyUnicode_NONCOMPACT_DATA(op))
(ซึ่งน่าเบื่อด้วยเหตุผลที่คล้ายกันโดยสมมติว่ามาโคร ( Something_CAPITALIZED
) ทั้งหมดทำงานเร็ว)
((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)
ซึ่ง ได้แก่
(((PyASCIIObject*)(op))->state.compact)
(เร็วเหมือนเดิม),
(PyUnicode_IS_ASCII(op) ? \
((void*)((PyASCIIObject*)(op) + 1)) : \
((void*)((PyCompactUnicodeObject*)(op) + 1)))
(เร็วถ้ามาโครIS_ASCII
เร็ว) และ
(assert(((PyUnicodeObject*)(op))->data.any), \
((((PyUnicodeObject *)(op))->data.any)))
(เร็วเช่นกันเพราะเป็นการยืนยันบวกทิศทางบวกนักแสดง)
ดังนั้นเราจึงลง (โพรงกระต่าย) เพื่อ:
PyUnicode_IS_ASCII
ซึ่งเป็น
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject*)op)->state.ascii)
อืม ... เร็วเหมือนกันนะ ...
ดีตกลง PyList_GetItem
แต่ขอเปรียบเทียบกับ (ใช่ขอบคุณ Tim Peters ที่มอบงานให้ฉันทำมากขึ้น: 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
เป็น
PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)
( แท็บ! แท็บ !!! ) ( issue21587 )ที่ได้รับการแก้ไขและรวมอยู่ใน5 นาที ชอบ ... เย้. ประณาม. พวกเขาทำให้ Skeet อับอาย
ดังนั้นนี่จึงเป็นเรื่องเล็กน้อยจริงๆ (สองคำสั่งและการตรวจสอบบูลีนสองสามรายการ) เว้นแต่Py_LIMITED_API
จะเปิดอยู่ในกรณีใด ... ???
จากนั้นก็มีการสร้างดัชนีและ cast ( ((PyListObject *)op) -> ob_item[i]
) และเราก็ทำ
ดังนั้นจึงมีการตรวจสอบรายการน้อยลงอย่างแน่นอนและความแตกต่างของความเร็วเล็กน้อยก็บ่งบอกได้อย่างแน่นอนว่าอาจเกี่ยวข้อง
ฉันคิดว่าโดยทั่วไปมีเพียงการตรวจสอบประเภทและทิศทาง(->)
สำหรับ Unicode มากกว่า ดูเหมือนว่าฉันกำลังพลาดประเด็นไป แต่อะไรนะ ?