TL; DR
Chênh lệch tốc độ thực tế gần hơn 70% (hoặc hơn) sau khi đã loại bỏ rất nhiều chi phí, đối với Python 2.
Tạo đối tượng không có lỗi. Cả hai phương thức đều không tạo ra một đối tượng mới, vì các chuỗi một ký tự được lưu trữ.
Sự khác biệt là không rõ ràng, nhưng có khả năng được tạo ra từ số lượng kiểm tra lớn hơn về lập chỉ mục chuỗi, liên quan đến loại và hình thức tốt. Nó cũng hoàn toàn có khả năng nhờ vào nhu cầu kiểm tra những gì sẽ trở lại.
Danh sách chỉ mục là rất nhanh.
>>> 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
Điều này không đồng ý với những gì bạn đã tìm thấy ...
Bạn phải sử dụng Python 2, sau đó.
>>> 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
Hãy giải thích sự khác biệt giữa các phiên bản. Tôi sẽ kiểm tra mã được biên dịch.
Đối với 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
Bạn thấy ở đây rằng biến thể danh sách có khả năng chậm hơn do việc xây dựng danh sách mỗi lần.
Đây là
9 LOAD_CONST 3 ('a')
12 LOAD_CONST 4 ('b')
15 LOAD_CONST 5 ('c')
18 BUILD_LIST 3
phần. Biến thể chuỗi chỉ có
9 LOAD_CONST 3 ('abc')
Bạn có thể kiểm tra xem điều này dường như tạo ra sự khác biệt:
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
Điều này tạo ra chỉ
9 LOAD_CONST 6 (('a', 'b', 'c'))
như tuples là bất biến. Kiểm tra:
>>> python3 -m timeit '[x for x in ("a", "b", "c")]'
1000000 loops, best of 3: 0.369 usec per loop
Tuyệt vời, sao lưu tốc độ.
Đối với 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
Điều kỳ lạ là chúng ta có cùng một tòa nhà trong danh sách, nhưng nó vẫn nhanh hơn cho việc này. Python 2 đang hành động rất nhanh.
Hãy loại bỏ sự hiểu biết và thời gian lại. Các _ =
là để ngăn chặn nó nhận được tối ưu hóa ra.
>>> 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
Chúng ta có thể thấy rằng việc khởi tạo không đủ quan trọng để tính đến sự khác biệt giữa các phiên bản (những con số này là nhỏ)! Do đó, chúng ta có thể kết luận rằng Python 3 có khả năng hiểu chậm hơn. Điều này có ý nghĩa khi Python 3 thay đổi cách hiểu để có phạm vi an toàn hơn.
Chà, bây giờ hãy cải thiện điểm chuẩn (Tôi chỉ loại bỏ chi phí không lặp lại). Điều này loại bỏ việc xây dựng các vòng lặp bằng cách gán trước nó:
>>> 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
Chúng tôi có thể kiểm tra xem cuộc gọi iter
có phải là chi phí không:
>>> 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
Không, không, không phải vậy. Sự khác biệt là quá nhỏ, đặc biệt là đối với Python 3.
Vì vậy, hãy loại bỏ nhiều chi phí không mong muốn hơn ... bằng cách làm cho toàn bộ điều chậm hơn! Mục đích chỉ là để có một vòng lặp dài hơn để thời gian ẩn trên đầu.
>>> 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
Điều này thực sự không thay đổi nhiều , nhưng nó đã giúp một chút.
Vì vậy, loại bỏ sự hiểu biết. Đó không phải là một phần của câu hỏi:
>>> 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
Tốt hơn rồi đấy! Chúng ta có thể nhanh hơn một chút bằng cách sử dụng deque
để lặp lại. Về cơ bản là giống nhau, nhưng nó nhanh hơn :
>>> 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
Điều gây ấn tượng với tôi là Unicode có khả năng cạnh tranh với bytestrings. Chúng tôi có thể kiểm tra điều này một cách rõ ràng bằng cách thử bytes
và unicode
trong cả hai:
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
Ở đây bạn thấy Python 3 thực sự nhanh hơn 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
Một lần nữa, Python 3 nhanh hơn, mặc dù điều này được mong đợi ( str
đã có rất nhiều sự chú ý trong Python 3).
Trong thực tế, điều này unicode
-bytes
sự khác biệt là rất nhỏ, rất ấn tượng.
Vì vậy, hãy phân tích trường hợp này, xem nó nhanh và tiện lợi cho tôi:
>>> 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
Chúng tôi thực sự có thể loại trừ câu trả lời được nâng cấp 10 lần của Tim Peter!
>>> foo = iterable[123]
>>> iterable[36] is foo
True
Đây không phải là những đối tượng mới!
Nhưng đây là điều đáng nói: chỉ số chi phí . Sự khác biệt có thể sẽ nằm ở việc lập chỉ mục, vì vậy hãy loại bỏ phép lặp và chỉ mục:
>>> 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
Sự khác biệt có vẻ nhỏ, nhưng ít nhất một nửa chi phí là chi phí:
>>> 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
Vì vậy, sự khác biệt tốc độ là đủ để quyết định đổ lỗi cho nó. Tôi nghĩ.
Vậy tại sao việc lập chỉ mục một danh sách nhanh hơn nhiều?
Chà, tôi sẽ quay lại với bạn về điều đó, nhưng tôi đoán đó là việc kiểm tra các chuỗi được thực hiện (hoặc các ký tự được lưu trong bộ nhớ cache nếu đó là một cơ chế riêng biệt). Điều này sẽ nhanh hơn tối ưu. Nhưng tôi sẽ đi kiểm tra nguồn (mặc dù tôi không thoải mái trong C ...) :).
Vì vậy, đây là nguồn:
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;
}
Đi bộ từ đầu, chúng tôi sẽ có một số kiểm tra. Đây là những nhàm chán. Sau đó, một số bài tập, cũng nên nhàm chán. Dòng thú vị đầu tiên là
ch = PyUnicode_READ(kind, data, index);
nhưng chúng tôi hy vọng điều đó sẽ nhanh, vì chúng tôi đang đọc từ một mảng C liền kề bằng cách lập chỉ mục cho nó. Kết quả, ch
sẽ nhỏ hơn 256 vì vậy chúng tôi sẽ trả lại ký tự được lưu trong bộ nhớ cache get_latin1_char(ch)
.
Vì vậy, chúng tôi sẽ chạy (bỏ các kiểm tra đầu tiên)
kind = PyUnicode_KIND(self);
data = PyUnicode_DATA(self);
ch = PyUnicode_READ(kind, data, index);
return get_latin1_char(ch);
Ở đâu
#define PyUnicode_KIND(op) \
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject *)(op))->state.kind)
(điều này thật nhàm chán vì các xác nhận bị bỏ qua trong gỡ lỗi [vì vậy tôi có thể kiểm tra xem chúng có nhanh không] và ((PyASCIIObject *)(op))->state.kind)
(tôi nghĩ) là một sự gián tiếp và diễn viên cấp C);
#define PyUnicode_DATA(op) \
(assert(PyUnicode_Check(op)), \
PyUnicode_IS_COMPACT(op) ? _PyUnicode_COMPACT_DATA(op) : \
_PyUnicode_NONCOMPACT_DATA(op))
(cũng nhàm chán vì những lý do tương tự, giả sử các macro ( Something_CAPITALIZED
) đều nhanh),
#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)] \
) \
))
(liên quan đến các chỉ mục nhưng thực sự không chậm chút nào) và
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;
}
Điều này khẳng định sự nghi ngờ của tôi rằng:
Đây là bộ nhớ cache:
PyObject *unicode = unicode_latin1[ch];
Điều này nên được nhanh chóng. Nó if (!unicode)
không chạy, vì vậy nó thực sự tương đương trong trường hợp này với
PyObject *unicode = unicode_latin1[ch];
Py_INCREF(unicode);
return unicode;
Thành thật mà nói, sau khi kiểm tra assert
s rất nhanh (bằng cách vô hiệu hóa chúng [Tôi nghĩ rằng nó hoạt động trên các xác nhận cấp độ C ...]), các phần chậm duy nhất là:
PyUnicode_IS_COMPACT(op)
_PyUnicode_COMPACT_DATA(op)
_PyUnicode_NONCOMPACT_DATA(op)
Đó là:
#define PyUnicode_IS_COMPACT(op) \
(((PyASCIIObject*)(op))->state.compact)
(nhanh, như trước),
#define _PyUnicode_COMPACT_DATA(op) \
(PyUnicode_IS_ASCII(op) ? \
((void*)((PyASCIIObject*)(op) + 1)) : \
((void*)((PyCompactUnicodeObject*)(op) + 1)))
(nhanh nếu macro IS_ASCII
nhanh) và
#define _PyUnicode_NONCOMPACT_DATA(op) \
(assert(((PyUnicodeObject*)(op))->data.any), \
((((PyUnicodeObject *)(op))->data.any)))
(cũng nhanh như đó là một khẳng định cộng với một sự gián tiếp cộng với một diễn viên).
Vì vậy, chúng tôi xuống (lỗ thỏ) để:
PyUnicode_IS_ASCII
đó là
#define PyUnicode_IS_ASCII(op) \
(assert(PyUnicode_Check(op)), \
assert(PyUnicode_IS_READY(op)), \
((PyASCIIObject*)op)->state.ascii)
Hmm ... có vẻ cũng nhanh ...
Chà, OK, nhưng hãy so sánh nó với PyList_GetItem
. (Vâng, cảm ơn Tim Peters đã cho tôi nhiều việc phải làm: 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];
}
Chúng ta có thể thấy rằng trong các trường hợp không có lỗi, điều này sẽ chạy:
PyList_Check(op)
Py_SIZE(op)
((PyListObject *)op) -> ob_item[i]
Trong trường hợp PyList_Check
là
#define PyList_Check(op) \
PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)
( TABS! TABS !!! ) ( số21587 ) Điều đó đã được sửa và hợp nhất trong 5 phút . Giống như ... vâng. Chỉ trích. Họ đặt Skeet vào sự xấu hổ.
#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
Vì vậy, điều này thường thực sự tầm thường (hai lần kiểm tra và một vài kiểm tra boolean) trừ khi Py_LIMITED_API
được bật, trong trường hợp nào ... ???
Sau đó, có chỉ mục và diễn viên (((PyListObject *)op) -> ob_item[i]
) và chúng ta đã hoàn thành.
Vì vậy, chắc chắn có ít kiểm tra hơn cho danh sách, và sự khác biệt tốc độ nhỏ chắc chắn ngụ ý rằng nó có thể có liên quan.
Tôi nghĩ nói chung, chỉ có nhiều loại kiểm tra và xác (->)
định đối với Unicode. Có vẻ như tôi đang thiếu một điểm, nhưng những gì ?