Tại sao việc lặp lại qua một chuỗi nhỏ chậm hơn một danh sách nhỏ?


132

Tôi đã chơi xung quanh với timeit và nhận thấy rằng việc hiểu một danh sách đơn giản trên một chuỗi nhỏ mất nhiều thời gian hơn so với thực hiện cùng một thao tác trên một danh sách các chuỗi ký tự nhỏ. Có lời giải thích nào không? Nó gần gấp 1,35 lần thời gian.

>>> from timeit import timeit
>>> timeit("[x for x in 'abc']")
2.0691067844831528
>>> timeit("[x for x in ['a', 'b', 'c']]")
1.5286479570345861

Điều gì đang xảy ra ở cấp độ thấp hơn gây ra điều này?

Câu trả lời:


193

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 itercó 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ử bytesunicodetrong 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ả, chsẽ 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 asserts 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_ASCIInhanh) 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

#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ì ?


17
Bạn đang trình bày mã là tự giải thích; bạn thậm chí còn trình bày các đoạn như kết luận. Thật không may cho tôi, tôi không thể thực sự theo nó. Không nói rằng cách tiếp cận của bạn để tìm ra những gì sai không phải là vững chắc, nhưng sẽ tốt hơn nếu nó dễ theo dõi hơn.
PascalVKooten

2
Tôi đã cố gắng cải thiện nó, nhưng tôi không chắc làm thế nào để làm cho nó rõ ràng hơn. Lưu ý rằng tôi không viết C, vì vậy đây là phân tích cấp cao về mã và chỉ các khái niệm tổng thể là quan trọng.
Veedrac

@ Tôi đã thêm. Hãy nói cho tôi nếu nó cảm thấy thiếu. Thật không may, nó cũng nhấn mạnh rằng tôi thực sự không biết câu trả lời (* gasp *).
Veedrac

3
Tôi sẽ đưa ra điều này một ngày khác trước khi tôi chấp nhận câu trả lời của bạn (tôi rất muốn thấy một cái gì đó cụ thể hơn bật lên), nhưng cảm ơn bạn vì câu trả lời rất thú vị và được nghiên cứu kỹ lưỡng.
Sunjay Varma

4
Lưu ý rằng bạn đang bắn vào một mục tiêu đang di chuyển ;-) Việc triển khai này không chỉ khác nhau giữa Python 2 và Python 3, mà còn giữa các bản phát hành khác nhau. Ví dụ, trên thân phát triển hiện tại, get_latin1_char()thủ thuật không còn tồn tại unicode_getitem(), mà ở cấp thấp hơn unicode_char. Vì vậy, có một cấp độ khác của hàm gọi ngay bây giờ - hoặc không (tùy thuộc vào trình biên dịch và cờ tối ưu hóa được sử dụng). Ở cấp độ chi tiết này, đơn giản là không có câu trả lời đáng tin cậy ;-)
Tim Peters

31

Khi bạn lặp lại trên hầu hết các đối tượng chứa (danh sách, bộ dữ liệu, ký tự, ...), trình vòng lặp sẽ cung cấp các đối tượng trong vùng chứa.

Nhưng khi bạn lặp lại một chuỗi, một đối tượng mới phải được tạo cho mỗi ký tự được phân phối - một chuỗi không phải là "một container" theo nghĩa tương tự một danh sách là một container. Các ký tự riêng lẻ trong một chuỗi không tồn tại dưới dạng các đối tượng riêng biệt trước khi lặp lại tạo ra các đối tượng đó.


3
Tôi thực sự không nghĩ rằng điều này là đúng. Bạn có thể kiểm tra với is. Nó nghe có vẻ đúng, nhưng tôi thực sự không nghĩ rằng nó có thể được.
Veedrac

Hãy xem @Veedrac trả lời.
Christian

3
stringobject.ccho thấy __getitem__đối với các chuỗi chỉ lấy kết quả từ bảng các chuỗi 1 ký tự được lưu trữ, do đó, chi phí phân bổ cho các chuỗi chỉ phát sinh một lần.
user2357112 hỗ trợ Monica

10
@ user2357112, vâng, đối với các chuỗi đơn giản trong Python 2 đó là một điểm quan trọng. Trong Python 3, tất cả các chuỗi đều là Unicode "chính thức" và có nhiều chi tiết hơn có liên quan (xem câu trả lời của Veedrac). Ví dụ, trong Python 3, sau đó s = chr(256), s is chr(256)trả về False- chỉ biết loại đó là không đủ, bởi vì các trường hợp đặc biệt tồn tại dưới vỏ bọc kích hoạt các giá trị dữ liệu .
Tim Peters

1

Bạn có thể phải chịu trách nhiệm và chi phí cho việc tạo iterator cho chuỗi. Trong khi đó mảng đã chứa một iterator khi khởi tạo.

BIÊN TẬP:

>>> timeit("[x for x in ['a','b','c']]")
0.3818681240081787
>>> timeit("[x for x in 'abc']")
0.3732869625091553

Điều này đã được chạy bằng 2.7, nhưng trên mac book pro i7 của tôi. Đây có thể là kết quả của sự khác biệt cấu hình hệ thống.


Ngay cả khi chỉ sử dụng các trình vòng lặp thẳng, chuỗi vẫn chậm hơn đáng kể. timeit ("[x cho x trong đó]", "it = iter ('abc')") = 0.34543599384033535; timeit ("[x for x in it]", "it = iter (list ('abc'))") = 0.2791691380446508
Sunjay Varma
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.