왜 'x'( 'x')가 'x'== 'x'보다 빠릅니까?


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


167
만일을 위해 : in대신에 다른 곳에서 사용하지 마십시오 ==. 가독성에 해를 끼치는 조기 최적화입니다.
대령 삼십 2

4
시도 x ="!foo" x in ("!foo",)하고x == "!foo"
Padraic Cunningham

2
B in A = Value, C == D Value 및 유형 비교
dsgdfg

6
in대신에 사용 하는 것보다 더 합리적인 방법 ==은 C로 전환하는 것입니다.
Mad Physicist

1
파이썬으로 작성 중이고 속도를 위해 하나의 구조를 선택하는 경우 잘못하고 있습니다.
Veky

답변:


257

데이비드 울 레버 (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();
}

스택에서 값을 팝합니다 (기술적으로는 하나만 팝)

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_LeaveRecursiveCall콤보는 이전 경로에서 촬영되지 않지만, 이들은 상대적으로 빠른 매크로되는거야 증가 및 일부 전역을 감소시키는 후 단락.

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

vs

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그들은 단지 필드의 몇 가지를 확인 꽤 저렴하지만 상단의 하나가 작은 코드 경로입니다 분명해야한다, 그것은 적은 함수 호출, 하나의 스위치 문이 단지 비트 얇다.

TL; DR :

둘 다 if (left_pointer == right_pointer); 차이점은 그들이 그곳에 가기 위해 얼마나 많은 일을하는지입니다. in그냥 덜합니다.


18
이것은 놀라운 대답입니다. 파이썬 프로젝트와의 관계는 무엇입니까?
kdbanman

9
@kdbanman 없음, 실제로 조금씩 내 길강요 했지만 ;).
Veedrac

21
@ varepsilon Aww, 그러나 아무도 실제 게시물을 감추고 귀찮게하지 않았습니다! 질문의 요점은 실제로 대답이 아니지만 대답에 도달하는 데 사용되는 프로세스 입니다. 프로덕션 에서이 해킹을 사용하는 사람들은 많지 않을 것입니다!
Veedrac

181

이 놀라운 행동을 만들어내는 세 가지 요소가 있습니다.

첫째 : in연산자는 단축키 ( x is y)를 확인하기 전에 등식 ( x == y)을 확인합니다 .

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

둘째, 파이썬의 문자열 인턴 때문에, 모두 "x"의의는 "x" in ("x", )동일합니다 :

>>> "x" is "x"
True

(큰 경고 :이 구현 고유의 동작입니다! is해야 결코 그것 때문에 문자열을 비교하는 데 사용되지 않습니다 때때로 놀라운 답변을 제공, 예를 들어 "x" * 100 is "x" * 100 ==> False)

셋째 :에 설명 된대로 Veedrac의 환상적인 대답 , tuple.__contains__( x in (y, )이다 대략 에 동등한 (y, ).__contains__(x)보다 빠른 신원 확인을 수행하는 지점에 도달) str.__eq__(다시, x == y이다 대략 에 해당 x.__eq__(y)하지 않습니다).

x in (y, )논리적으로 동등한 것보다 상당히 느리기 때문에 이에 대한 증거를 볼 수 있습니다 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 == b보다 꽤 많은 VM 지침이 될 것입니다)

Veedrac의 대답 - https://stackoverflow.com/a/28889838/71522은 - 각시 발생 구체적으로 무엇에 더 많은 세부로 전환 ==하고 in및 읽기 가치가있다.


3
그리고 그 이유는이 허용 할 가능성이 높습니다하지 X in [X,Y,Z]않고 제대로 작동하려면 X, Y또는 Z평등 방법을 정의 할 필요 (또는 오히려, 기본 같음은 is그것을 호출 할 필요 절약 할 수 있도록, __eq__어떤 사용자 정의 개체에 __eq__is진실되고하는 것을 의미한다 값 -평등).
aruisdante

1
의 사용은 float('nan')오도의 소지가 있습니다. 그것은 nan자신과 같지 않다는 속성입니다 . 즉 수 있습니다 타이밍을 변경합니다.
임마

@dawg 아, 좋은 지적-난 예제는 in회원 테스트에 대한 지름길을 설명하기위한 것입니다 . 변수 이름을 명확하게 변경하겠습니다.
David Wolever

3
내가 이해하는 한, CPython 3.4.3 tuple.__contains__에서는 tuplecontains어떤 호출에 의해 구현되며 PyObject_RichCompareBoolID의 경우 즉시 반환됩니다. unicodePyUnicode_RichCompare정체성에 대해 동일한 바로 가기가 후드 아래.
Cristian Ciupitu

3
"x" is "x"반드시 그런 것은 아닙니다 True. 'x' in ('x', )항상 True이지만보다 빠르지 않은 것처럼 보일 수 있습니다 ==.
David Wolever
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.