เหตุใดการวนซ้ำบนสตริงขนาดเล็กจึงช้ากว่ารายการขนาดเล็ก


132

ฉันกำลังเล่นกับเวลาและสังเกตว่าการทำความเข้าใจรายการอย่างง่ายบนสตริงขนาดเล็กใช้เวลานานกว่าการดำเนินการแบบเดียวกันกับรายการสตริงอักขระเดี่ยวขนาดเล็ก คำอธิบายใด ๆ เป็นเวลาเกือบ 1.35 เท่า

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

เกิดอะไรขึ้นในระดับล่างที่ทำให้เกิดสิ่งนี้

คำตอบ:


193

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)
#>>>   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

คุณจะเห็นว่าตัวแปรรายการมีแนวโน้มที่จะช้าลงเนื่องจากการสร้างรายการในแต่ละครั้ง

นี้เป็น

 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)
#>>>  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

เพียงแค่นี้

 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)
#>>>   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        

สิ่งที่แปลกคือเรามีสิ่งปลูกสร้างเดียวกันของรายการ แต่ก็ยังเร็วกว่าสำหรับสิ่งนี้ 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);

ที่ไหน

#define PyUnicode_KIND(op) \
    (assert(PyUnicode_Check(op)), \
     assert(PyUnicode_IS_READY(op)),            \
     ((PyASCIIObject *)(op))->state.kind)

(ซึ่งน่าเบื่อเพราะการยืนยันถูกเพิกเฉยในการดีบั๊ก [ดังนั้นฉันจึงตรวจสอบได้ว่ามันเร็ว] และ((PyASCIIObject *)(op))->state.kind)(ฉันคิดว่า) เป็นทิศทางและการร่ายระดับ C);

#define PyUnicode_DATA(op) \
    (assert(PyUnicode_Check(op)), \
     PyUnicode_IS_COMPACT(op) ? _PyUnicode_COMPACT_DATA(op) :   \
     _PyUnicode_NONCOMPACT_DATA(op))

(ซึ่งน่าเบื่อด้วยเหตุผลที่คล้ายกันโดยสมมติว่ามาโคร ( Something_CAPITALIZED) ทั้งหมดทำงานเร็ว)

#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)] \
        ) \
    ))

(ซึ่งเกี่ยวข้องกับดัชนี แต่จริงๆแล้วไม่ได้ช้าเลย) และ

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;
    

จริงๆแล้วหลังจากการทดสอบasserts นั้นเร็ว (โดยการปิดการใช้งาน [ฉันคิดว่ามันใช้ได้กับการยืนยันระดับ C ... ]) ส่วนที่ช้าอย่างน่าจะเป็นเพียง:

PyUnicode_IS_COMPACT(op)
_PyUnicode_COMPACT_DATA(op)
_PyUnicode_NONCOMPACT_DATA(op)

ซึ่ง ได้แก่

#define PyUnicode_IS_COMPACT(op) \
    (((PyASCIIObject*)(op))->state.compact)

(เร็วเหมือนเดิม),

#define _PyUnicode_COMPACT_DATA(op)                     \
    (PyUnicode_IS_ASCII(op) ?                   \
     ((void*)((PyASCIIObject*)(op) + 1)) :              \
     ((void*)((PyCompactUnicodeObject*)(op) + 1)))

(เร็วถ้ามาโครIS_ASCIIเร็ว) และ

#define _PyUnicode_NONCOMPACT_DATA(op)                  \
    (assert(((PyUnicodeObject*)(op))->data.any),        \
     ((((PyUnicodeObject *)(op))->data.any)))

(เร็วเช่นกันเพราะเป็นการยืนยันบวกทิศทางบวกนักแสดง)

ดังนั้นเราจึงลง (โพรงกระต่าย) เพื่อ:

PyUnicode_IS_ASCII

ซึ่งเป็น

#define PyUnicode_IS_ASCII(op)                   \
    (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เป็น

#define PyList_Check(op) \
     PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)

( แท็บ! แท็บ !!! ) ( issue21587 )ที่ได้รับการแก้ไขและรวมอยู่ใน5 นาที ชอบ ... เย้. ประณาม. พวกเขาทำให้ 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

ดังนั้นนี่จึงเป็นเรื่องเล็กน้อยจริงๆ (สองคำสั่งและการตรวจสอบบูลีนสองสามรายการ) เว้นแต่Py_LIMITED_APIจะเปิดอยู่ในกรณีใด ... ???

จากนั้นก็มีการสร้างดัชนีและ cast ( ((PyListObject *)op) -> ob_item[i]) และเราก็ทำ

ดังนั้นจึงมีการตรวจสอบรายการน้อยลงอย่างแน่นอนและความแตกต่างของความเร็วเล็กน้อยก็บ่งบอกได้อย่างแน่นอนว่าอาจเกี่ยวข้อง


ฉันคิดว่าโดยทั่วไปมีเพียงการตรวจสอบประเภทและทิศทาง(->)สำหรับ Unicode มากกว่า ดูเหมือนว่าฉันกำลังพลาดประเด็นไป แต่อะไรนะ ?


17
คุณกำลังนำเสนอรหัสที่อธิบายได้ด้วยตนเอง คุณกำลังนำเสนอตัวอย่างข้อมูลเป็นข้อสรุป น่าเสียดายสำหรับฉันฉันไม่สามารถติดตามมันได้จริงๆ ไม่ได้บอกว่าแนวทางของคุณในการค้นหาว่ามีอะไรผิดปกตินั้นไม่มั่นคง แต่จะดีถ้าทำตามได้ง่ายกว่า
PascalVKooten

2
ฉันพยายามปรับปรุง แต่ไม่แน่ใจว่าจะทำให้ชัดเจนขึ้นได้อย่างไร โปรดทราบว่าฉันไม่ได้เขียน C ดังนั้นนี่คือการวิเคราะห์โค้ดระดับสูงและมีเพียงแนวคิดโดยรวมเท่านั้นที่สำคัญ
Veedrac

@Nit ฉันได้เพิ่ม บอกฉันว่ารู้สึกขาด น่าเสียดายที่มันยังเน้นว่าฉันไม่รู้คำตอบจริงๆ (* อ้าปากค้าง *)
Veedrac

3
ฉันจะให้สิ่งนี้อีกวันก่อนที่ฉันจะยอมรับคำตอบของคุณ (ฉันอยากเห็นอะไรที่เป็นรูปธรรมมากกว่านี้) แต่ขอขอบคุณสำหรับคำตอบที่น่าสนใจและมีการค้นคว้ามาเป็นอย่างดี
Sunjay Varma

4
โปรดทราบว่าคุณกำลังยิงเป้าหมายที่กำลังเคลื่อนที่ ;-) การใช้งานนี้ไม่เพียง แต่แตกต่างกันระหว่าง Python 2 และ Python 3 เท่านั้น แต่ยังรวมถึงรุ่นต่างๆ ตัวอย่างเช่นในลำต้นการพัฒนาปัจจุบันget_latin1_char()เคล็ดลับไม่มีอยู่แล้วunicode_getitem()แต่อยู่ในระดับunicode_charล่าง ดังนั้นจึงมีการเรียกใช้ฟังก์ชันอีกระดับหนึ่งในขณะนี้หรือไม่ (ขึ้นอยู่กับแฟล็กคอมไพเลอร์และการปรับให้เหมาะสมที่ใช้) ในรายละเอียดระดับนี้ไม่มีคำตอบที่เชื่อถือได้
Tim Peters

31

เมื่อคุณวนซ้ำบนวัตถุคอนเทนเนอร์ส่วนใหญ่ (รายการ, สิ่งที่เพิ่มขึ้น, คำสั่ง, ... ) ตัววนซ้ำจะส่งวัตถุในคอนเทนเนอร์

แต่เมื่อคุณวนซ้ำบนสตริงจะต้องสร้างออบเจ็กต์ใหม่สำหรับอักขระแต่ละตัวที่ส่งมา - สตริงไม่ใช่ "คอนเทนเนอร์" ในความหมายเดียวกันกับรายการคือคอนเทนเนอร์ อักขระแต่ละตัวในสตริงไม่มีอยู่เป็นอ็อบเจ็กต์ที่แตกต่างกันก่อนการวนซ้ำจะสร้างอ็อบเจ็กต์เหล่านั้น


3
ฉันไม่คิดว่านี่เป็นเรื่องจริงที่จริง isคุณสามารถตรวจสอบกับ มันฟังดูดี แต่ฉันไม่คิดว่ามันจะเป็นไปได้
Veedrac

ลองดูคำตอบของ @Veedrac
คริสเตียน

3
stringobject.cแสดงให้เห็นว่า__getitem__สำหรับสตริงเพียงแค่ดึงผลลัพธ์จากตารางของสตริงที่เก็บไว้ 1 อักขระดังนั้นค่าใช้จ่ายในการปันส่วนสำหรับสตริงจะเกิดขึ้นเพียงครั้งเดียว
user2357112 รองรับ Monica

10
@ user2357112 ใช่สำหรับสตริงธรรมดาใน Python 2 นั่นเป็นจุดสำคัญ ใน Python 3 สตริงทั้งหมดเป็น Unicode "อย่างเป็นทางการ" และมีรายละเอียดเพิ่มเติมอีกมากมายที่เกี่ยวข้อง (ดูคำตอบของ Veedrac) ยกตัวอย่างเช่นในงูใหญ่ 3 หลังs = chr(256), s is chr(256)ผลตอบแทนFalse- รู้ชนิดอย่างเดียวไม่พอเพราะกองกรณีพิเศษอยู่ภายใต้ครอบคลุมการทริกข้อมูลค่า
Tim Peters

1

คุณอาจมีค่าใช้จ่ายและค่าใช้จ่ายในการสร้างตัววนซ้ำสำหรับสตริง ในขณะที่อาร์เรย์มีตัววนซ้ำอยู่แล้วในการสร้างอินสแตนซ์

แก้ไข:

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

นี่วิ่งโดยใช้ 2.7 แต่บน mac book pro i7 ของฉัน นี่อาจเป็นผลมาจากความแตกต่างของการกำหนดค่าระบบ


แม้แต่การใช้ตัวทำซ้ำแบบตรงสตริงก็ยังช้ากว่าอย่างเห็นได้ชัด timeit ("[x สำหรับ x อยู่ในนั้น]", "it = iter ('abc')") = 0.34543599384033535; timeit ("[x สำหรับ x อยู่ในนั้น]", "it = iter (list ('abc'))") = 0.2791691380446508
Sunjay Varma
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.