Tại sao 'x' in ('x',) nhanh hơn 'x' == 'x'?


274
>>> timeit.timeit("'x' in ('x',)")
0.04869917374131205
>>> timeit.timeit("'x' == 'x'")
0.06144205736110564

Cũng hoạt động cho các bộ dữ liệu có nhiều yếu tố, cả hai phiên bản dường như phát triển tuyến tính:

>>> timeit.timeit("'x' in ('x', 'y')")
0.04866674801541748
>>> timeit.timeit("'x' == 'x' or 'x' == 'y'")
0.06565782838087131
>>> timeit.timeit("'x' in ('y', 'x')")
0.08975995576448526
>>> timeit.timeit("'x' == 'y' or 'x' == 'y'")
0.12992391047427532

Dựa trên điều này, tôi nghĩ rằng tôi hoàn toàn nên bắt đầu sử dụng inở mọi nơi thay vì ==!


167
Chỉ trong trường hợp: Vui lòng không bắt đầu sử dụng inở mọi nơi thay vì ==. Đó là một tối ưu hóa sớm gây hại cho khả năng đọc.
Đại tá Ba mươi Hai

4
thử x ="!foo" x in ("!foo",)x == "!foo"
Padraic Cickyham

2
A trong B = Giá trị, C == D So sánh giá trị và loại
dsgdfg

6
Một cách tiếp cận hợp lý hơn là sử dụng inthay vì ==chuyển sang C.
Mad Physicist

1
Nếu bạn đang viết bằng Python và bạn chọn một cấu trúc trên tốc độ khác, thì bạn đã làm sai.
Veky

Câu trả lời:


257

Như tôi đã đề cập với David Wolever, có nhiều thứ hơn là bắt mắt; cả hai phương thức gửi đến is; bạn có thể chứng minh điều này bằng cách làm

min(Timer("x == x", setup="x = 'a' * 1000000").repeat(10, 10000))
#>>> 0.00045456900261342525

min(Timer("x == y", setup="x = 'a' * 1000000; y = 'a' * 1000000").repeat(10, 10000))
#>>> 0.5256857610074803

Việc đầu tiên chỉ có thể nhanh như vậy bởi vì nó kiểm tra theo danh tính.

Để tìm hiểu lý do tại sao một người sẽ mất nhiều thời gian hơn người khác, hãy theo dõi thông qua thực hiện.

Cả hai đều bắt đầu ceval.c, COMPARE_OPvì đó là mã byte liên quan

TARGET(COMPARE_OP) {
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *res = cmp_outcome(oparg, left, right);
    Py_DECREF(left);
    Py_DECREF(right);
    SET_TOP(res);
    if (res == NULL)
        goto error;
    PREDICT(POP_JUMP_IF_FALSE);
    PREDICT(POP_JUMP_IF_TRUE);
    DISPATCH();
}

Điều này bật các giá trị từ ngăn xếp (về mặt kỹ thuật nó chỉ bật một)

PyObject *right = POP();
PyObject *left = TOP();

và chạy so sánh:

PyObject *res = cmp_outcome(oparg, left, right);

cmp_outcome có phải đây là:

static PyObject *
cmp_outcome(int op, PyObject *v, PyObject *w)
{
    int res = 0;
    switch (op) {
    case PyCmp_IS: ...
    case PyCmp_IS_NOT: ...
    case PyCmp_IN:
        res = PySequence_Contains(w, v);
        if (res < 0)
            return NULL;
        break;
    case PyCmp_NOT_IN: ...
    case PyCmp_EXC_MATCH: ...
    default:
        return PyObject_RichCompare(v, w, op);
    }
    v = res ? Py_True : Py_False;
    Py_INCREF(v);
    return v;
}

Đây là nơi các đường dẫn phân chia. Các PyCmp_INchi nhánh không

int
PySequence_Contains(PyObject *seq, PyObject *ob)
{
    Py_ssize_t result;
    PySequenceMethods *sqm = seq->ob_type->tp_as_sequence;
    if (sqm != NULL && sqm->sq_contains != NULL)
        return (*sqm->sq_contains)(seq, ob);
    result = _PySequence_IterSearch(seq, ob, PY_ITERSEARCH_CONTAINS);
    return Py_SAFE_DOWNCAST(result, Py_ssize_t, int);
}

Lưu ý rằng một tuple được định nghĩa là

static PySequenceMethods tuple_as_sequence = {
    ...
    (objobjproc)tuplecontains,                  /* sq_contains */
};

PyTypeObject PyTuple_Type = {
    ...
    &tuple_as_sequence,                         /* tp_as_sequence */
    ...
};

Chi nhánh

if (sqm != NULL && sqm->sq_contains != NULL)

sẽ được thực hiện và *sqm->sq_contains, đó là chức năng (objobjproc)tuplecontains, sẽ được thực hiện.

Cái này không

static int
tuplecontains(PyTupleObject *a, PyObject *el)
{
    Py_ssize_t i;
    int cmp;

    for (i = 0, cmp = 0 ; cmp == 0 && i < Py_SIZE(a); ++i)
        cmp = PyObject_RichCompareBool(el, PyTuple_GET_ITEM(a, i),
                                           Py_EQ);
    return cmp;
}

... Đợi đã, đó không phải là PyObject_RichCompareBoolnhững gì các chi nhánh khác đã lấy? Không, đó là PyObject_RichCompare.

Đường dẫn mã đó ngắn nên có khả năng giảm tốc độ của hai thứ này. Hãy so sánh.

int
PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
{
    PyObject *res;
    int ok;

    /* Quick result when objects are the same.
       Guarantees that identity implies equality. */
    if (v == w) {
        if (op == Py_EQ)
            return 1;
        else if (op == Py_NE)
            return 0;
    }

    ...
}

Đường dẫn mã trong PyObject_RichCompareBoolkhá nhiều ngay lập tức chấm dứt. Đối với PyObject_RichCompare

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyObject *res;

    assert(Py_LT <= op && op <= Py_GE);
    if (v == NULL || w == NULL) { ... }
    if (Py_EnterRecursiveCall(" in comparison"))
        return NULL;
    res = do_richcompare(v, w, op);
    Py_LeaveRecursiveCall();
    return res;
}

Các Py_EnterRecursiveCall/ Py_LeaveRecursiveCallkết hợp không được thực hiện trong đường dẫn trước, nhưng đây là những macro tương đối nhanh chóng mà sẽ ngắn mạch sau khi tăng và giảm một số globals.

do_richcompare làm:

static PyObject *
do_richcompare(PyObject *v, PyObject *w, int op)
{
    richcmpfunc f;
    PyObject *res;
    int checked_reverse_op = 0;

    if (v->ob_type != w->ob_type && ...) { ... }
    if ((f = v->ob_type->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        ...
    }
    ...
}

Đây là một số kiểm tra nhanh để gọi v->ob_type->tp_richcompaređó là

PyTypeObject PyUnicode_Type = {
    ...
    PyUnicode_RichCompare,      /* tp_richcompare */
    ...
};

cái nào

PyObject *
PyUnicode_RichCompare(PyObject *left, PyObject *right, int op)
{
    int result;
    PyObject *v;

    if (!PyUnicode_Check(left) || !PyUnicode_Check(right))
        Py_RETURN_NOTIMPLEMENTED;

    if (PyUnicode_READY(left) == -1 ||
        PyUnicode_READY(right) == -1)
        return NULL;

    if (left == right) {
        switch (op) {
        case Py_EQ:
        case Py_LE:
        case Py_GE:
            /* a string is equal to itself */
            v = Py_True;
            break;
        case Py_NE:
        case Py_LT:
        case Py_GT:
            v = Py_False;
            break;
        default:
            ...
        }
    }
    else if (...) { ... }
    else { ...}
    Py_INCREF(v);
    return v;
}

Cụ thể, phím tắt này trên left == right... nhưng chỉ sau khi thực hiện

    if (!PyUnicode_Check(left) || !PyUnicode_Check(right))

    if (PyUnicode_READY(left) == -1 ||
        PyUnicode_READY(right) == -1)

Tất cả trong tất cả các đường dẫn sau đó trông giống như thế này (thủ công đệ quy nội tuyến, không kiểm soát và cắt tỉa các nhánh đã biết)

POP()                           # Stack stuff
TOP()                           #
                                #
case PyCmp_IN:                  # Dispatch on operation
                                #
sqm != NULL                     # Dispatch to builtin op
sqm->sq_contains != NULL        #
*sqm->sq_contains               #
                                #
cmp == 0                        # Do comparison in loop
i < Py_SIZE(a)                  #
v == w                          #
op == Py_EQ                     #
++i                             # 
cmp == 0                        #
                                #
res < 0                         # Convert to Python-space
res ? Py_True : Py_False        #
Py_INCREF(v)                    #
                                #
Py_DECREF(left)                 # Stack stuff
Py_DECREF(right)                #
SET_TOP(res)                    #
res == NULL                     #
DISPATCH()                      #

đấu với

POP()                           # Stack stuff
TOP()                           #
                                #
default:                        # Dispatch on operation
                                #
Py_LT <= op                     # Checking operation
op <= Py_GE                     #
v == NULL                       #
w == NULL                       #
Py_EnterRecursiveCall(...)      # Recursive check
                                #
v->ob_type != w->ob_type        # More operation checks
f = v->ob_type->tp_richcompare  # Dispatch to builtin op
f != NULL                       #
                                #
!PyUnicode_Check(left)          # ...More checks
!PyUnicode_Check(right))        #
PyUnicode_READY(left) == -1     #
PyUnicode_READY(right) == -1    #
left == right                   # Finally, doing comparison
case Py_EQ:                     # Immediately short circuit
Py_INCREF(v);                   #
                                #
res != Py_NotImplemented        #
                                #
Py_LeaveRecursiveCall()         # Recursive check
                                #
Py_DECREF(left)                 # Stack stuff
Py_DECREF(right)                #
SET_TOP(res)                    #
res == NULL                     #
DISPATCH()                      #

Bây giờ, PyUnicode_CheckPyUnicode_READYkhá rẻ vì họ chỉ kiểm tra một vài trường, nhưng rõ ràng là cái trên cùng là một đường dẫn mã nhỏ hơn, nó có ít lệnh gọi hàm hơn, chỉ có một câu lệnh chuyển đổi và chỉ mỏng hơn một chút.

TL; DR:

Cả phái đi if (left_pointer == right_pointer); sự khác biệt chỉ là có bao nhiêu công việc họ làm để đạt được điều đó. inchỉ làm ít thôi


18
Đây là một câu trả lời đáng kinh ngạc. Mối quan hệ của bạn với dự án python là gì?
kdbanman

9
@kdbanman Không, thực sự, mặc dù tôi đã cố gắng theo cách của mình một chút;).
Veedrac 13/03/2015

21
@varepsilon Aww, nhưng sau đó không ai muốn lướt qua bài viết thực tế! Điểm chính của câu hỏi không thực sự là câu trả lời mà là quá trình được sử dụng để đi đến câu trả lời - hy vọng sẽ không có quá nhiều người sử dụng bản hack này trong sản xuất!
Veedrac

181

Có ba yếu tố chơi ở đây, kết hợp lại, tạo ra hành vi đáng ngạc nhiên này.

Đầu tiên: intoán tử thực hiện một phím tắt và kiểm tra danh tính ( x is y) trước khi nó kiểm tra đẳng thức ( x == y):

>>> n = float('nan')
>>> n in (n, )
True
>>> n == n
False
>>> n is n
True

Thứ hai: do thực hiện chuỗi của Python, cả hai "x"trong "x" in ("x", )sẽ giống hệt nhau:

>>> "x" is "x"
True

(cảnh báo lớn: đây là hành vi thực hiện cụ thể! isnên không bao giờ được sử dụng để so sánh chuỗi bởi vì nó sẽ cung cấp cho câu trả lời đáng ngạc nhiên đôi khi, ví dụ "x" * 100 is "x" * 100 ==> False)

Thứ ba: như chi tiết trong câu trả lời tuyệt vời Veedrac của , tuple.__contains__( x in (y, )khoảng tương đương (y, ).__contains__(x)) được cho điểm thực hiện việc kiểm tra tính nhanh hơn str.__eq__(một lần nữa, x == yxấp xỉ tương đương với x.__eq__(y)) thực hiện.

Bạn có thể thấy bằng chứng cho điều này vì x in (y, )chậm hơn đáng kể so với tương đương logic , x == y:

In [18]: %timeit 'x' in ('x', )
10000000 loops, best of 3: 65.2 ns per loop

In [19]: %timeit 'x' == 'x'    
10000000 loops, best of 3: 68 ns per loop

In [20]: %timeit 'x' in ('y', ) 
10000000 loops, best of 3: 73.4 ns per loop

In [21]: %timeit 'x' == 'y'    
10000000 loops, best of 3: 56.2 ns per loop

Các x in (y, )trường hợp chậm hơn bởi vì, sau khi isso sánh thất bại, inđiều hành rơi trở lại để kiểm tra bình đẳng bình thường (ví dụ, sử dụng ==), do đó việc so sánh mất khoảng cùng một lượng thời gian như ==, khiến toàn bộ hoạt động chậm hơn vì chi phí của việc tạo ra các tuple , đi bộ các thành viên của nó, vv

Cũng lưu ý rằng a in (b, )chỉ nhanh hơn khi a is b:

In [48]: a = 1             

In [49]: b = 2

In [50]: %timeit a is a or a == a
10000000 loops, best of 3: 95.1 ns per loop

In [51]: %timeit a in (a, )      
10000000 loops, best of 3: 140 ns per loop

In [52]: %timeit a is b or a == b
10000000 loops, best of 3: 177 ns per loop

In [53]: %timeit a in (b, )      
10000000 loops, best of 3: 169 ns per loop

(tại sao a in (b, )nhanh hơn a is b or a == b? Tôi đoán sẽ có ít hướng dẫn máy ảo hơn -  a in (b, )chỉ ~ 3 hướng dẫn, trong đó a is b or a == bsẽ có thêm một vài hướng dẫn VM)

Veedrac của câu trả lời - https://stackoverflow.com/a/28889838/71522 - đi vào chi tiết hơn nhiều vào cụ thể những gì xảy ra trong mỗi ==invà rất đáng đọc.


3
Và lý do nó thực hiện điều này là khả năng cho phép X in [X,Y,Z]để làm việc một cách chính xác mà không X, Yhoặc Zcần phải xác định phương pháp bình đẳng (hay đúng hơn, sự bình đẳng mặc định là is, vì vậy nó tiết kiệm phải gọi điện __eq__trên các đối tượng không có người dùng định nghĩa __eq__islà sự thật nên bao hàm giá trị -đẳng thức).
aruonomante

1
Việc sử dụng float('nan')là tiềm năng gây hiểu lầm. Đó là một tài sản của nannó không bằng chính nó. Điều đó có thể thay đổi thời gian.
dawg

@dawg ah, điểm tốt - ví dụ nan chỉ nhằm minh họa lối tắt introng các bài kiểm tra thành viên. Tôi sẽ thay đổi tên biến để làm rõ.
David Wolever

3
Theo tôi hiểu, trong CPython 3.4.3 tuple.__contains__được thực hiện bằng cách tuplecontainsgọi PyObject_RichCompareBoolvà trả về ngay lập tức trong trường hợp nhận dạng. unicodePyUnicode_RichComparetrong mui xe, có cùng lối tắt cho danh tính.
Cristian Ciupitu

3
Nó có nghĩa "x" is "x"là không nhất thiết phải như vậy True. 'x' in ('x', )sẽ luôn luôn như vậy True, nhưng nó có thể không xuất hiện nhanh hơn ==.
David Wolever
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.