IT

'x'가 'x' == 'x'보다 빠른 이유는 무엇입니까?

itgroup 2022. 11. 18. 21:35
반응형

'x'가 'x' == 'x'보다 빠른 이유는 무엇입니까?

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

또한 여러 요소가 포함된 튜플에도 사용할 수 있으며, 두 버전은 선형적으로 증가하는 것으로 보입니다.

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

이 정도만 봐서는 이제 이 제품을 사용해서in대신 도처에==!

David Wolever에게 말했듯이, 이것에는 눈에 보이는 것 이상의 것이 있습니다; 두 가지 방법 모두 로 보내집니다.is; 이것을 증명하려면 , 다음의 조작을 실시합니다.

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

첫 번째는 아이덴티티에 의해 확인되기 때문에 매우 빠를 수 있습니다.

왜 한쪽이 다른 쪽보다 오래 걸리는지 알아보려면 실행을 통해 추적해 보겠습니다.

둘 다 시작해요.ceval.c,부터COMPARE_OP그것이 관련된 바이트 코드이기 때문에

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();
}

그러면 스택에서 값이 팝됩니다(기술적으로는 1개만 팝됩니다).

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

및 비교를 실행합니다.

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

cmp_outcome이것은 다음과 같습니다.

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;
}

여기가 길이 갈라진 곳이야PyCmp_IN브런치에서는 하고 있다

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);
}

태플은 다음과 같이 정의됩니다.

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

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

그래서 브랜치

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

사용되다*sqm->sq_contains이 함수는(objobjproc)tuplecontains이 사용됩니다.

이것은 그렇다

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;
}

잠깐, 그거 아니야?PyObject_RichCompareBool다른 나뭇가지에서 뭘 가져갔지?아니, 그건PyObject_RichCompare.

암호 경로가 짧아서 이 두 가지 속도로 귀결될 수 있습니다.비교해 봅시다.

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;
    }

    ...
}

의 코드 경로PyObject_RichCompareBool거의 즉시 종료됩니다.위해서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;
}

Py_EnterRecursiveCall/Py_LeaveRecursiveCallcombo는 이전 경로에서는 사용되지 않지만 글로벌 증감 후 단락되는 비교적 빠른 매크로입니다.

do_richcompare다음 작업을 수행합니다.

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;
        ...
    }
    ...
}

이것은 몇 가지 빠른 콜 체크를 수행합니다.v->ob_type->tp_richcompare어느 것이

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

어느 쪽인가 하면

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;
}

즉, 이 단축키는left == right...하지만 한 다음에야

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

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

모든 경로에서 이와 같은 상태가 됩니다(수동으로 재귀적으로 기존의 브랜치를 인라인화, 언롤, 플루닝).

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

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

지금이다,PyUnicode_Check그리고.PyUnicode_READY는 몇 개의 필드만 체크하기 때문에 매우 저렴합니다만, 상단의 것은 작은 코드 패스이며, 함수 호출이 적고, 스위치 문이 1개뿐이며, 조금 얇을 뿐이라는 것은 분명합니다.

TL;DR:

양쪽 디스패치 대상if (left_pointer == right_pointer); 그들이 거기에 도달하기 위해 얼마나 많은 일을 하느냐가 차이입니다. in덜 할 뿐입니다.

여기에는 세 가지 요인이 작용하여 이 놀라운 행동을 일으킵니다.

첫 번째:in오퍼레이터는 숏컷을 사용하여 ID를 확인합니다(x is y동등성을 체크하기 전에 ( )x == y):

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

둘째: Python의 문자열 인터닝으로 인해"x"에 있다"x" in ("x", )같게 됩니다.

>>> "x" is "x"
True

(큰 경고: 이것은 구현 고유의 동작입니다. is문자열 비교에는 사용하지 마십시오.가끔 놀라운 답을 얻을 수 있기 때문입니다.예를 들어,"x" * 100 is "x" * 100 ==> False)

번째: 비드락의 환상적인 답변에 따르면tuple.__contains__ )x in (y, )대략 와 동등하다(y, ).__contains__(x)는가 보다 .str.__eq__ 한 번x == y대략 와 동등하다x.__eq__(y)가 됩니다).가 됩니다.

을 볼 수요. 왜냐하면 이 증거들이 있다라는 거죠.x in (y, )으로 동등한 값인 리리으 is the the the the the the the the the the the the 보다 현저하게 .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

x in (y, )부터는 더.★★★★★★★★★★★★★★★★★★,is하여 " " " " 가in 체크로 "동일성 체크"를 합니다).==)이 때문에 에 걸리는 ), 。==태플 작성, 멤버 워킹 등의 오버헤드로 인해 동작 전체가 느려집니다.

, 「 」,a in (b, )보다 고속인 것은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

?)a in (b, ) a is b or a == b명령의 가 적어지는 것.- 가상 머신의 명령의 수가 적어집니다.a in (b, )서 3개요.a is b or a == bVM을 사용합니다).

Veedrac의 답변(https://stackoverflow.com/a/28889838/71522)은 특히 각 비즈니스에서 일어나는 일에 대해 훨씬 더 자세히 설명합니다.== ★★★★★★★★★★★★★★★★★」in읽을 가치가 있습니다.

언급URL : https://stackoverflow.com/questions/28885132/why-is-x-in-x-faster-than-x-x

반응형