'X' ('x') içindeki 'x' neden 'x' == 'x' den daha hızlı?


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

Ayrıca birden fazla öğeye sahip tuples için çalışır, her iki sürüm de doğrusal olarak büyür:

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

Buna dayanarak, ben düşünüyorum tamamen kullanmaya başlamak inyerine her yerde ==!


167
Her ihtimale karşı: Lütfen inbunun yerine her yerde kullanmaya başlamayın ==. Okunabilirliğe zarar veren erken bir optimizasyon.
Albay Otuz İki

4
deneyin x ="!foo" x in ("!foo",)vex == "!foo"
Padraic Cunningham

2
A = B = Değer, C == D Değer ve Tür karşılaştırması
dsgdfg

6
Kullanmak inyerine kullanmaktan daha mantıklı bir yaklaşım ==C'ye
geçmektir

1
Python'da yazıyorsanız ve hız için bir yapı diğerini seçerseniz, bunu yanlış yapıyorsunuz.
Veky

Yanıtlar:


257

David Wolever'a bahsettiğim gibi, bununla karşılaşmaktan daha fazlası var; her iki yöntem de gönderilir is; bunu yaparak kanıtlayabilirsin

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

Birincisi çok hızlı olabilir, çünkü kimlikle kontrol eder.

Birinin neden diğerinden daha uzun süreceğini öğrenmek için, yürütmeyi izleyelim.

İkisi de başlar ceval.c, COMPARE_OPçünkü bu ilgili bayt kodu

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

Bu, değerleri yığından çıkar (teknik olarak yalnızca bir tane çıkar)

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

ve karşılaştırmayı çalıştırır:

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

cmp_outcome bu:

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

Burası yolların ayrıldığı yerdir. PyCmp_INşube yapar

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

Bir demetin şu şekilde tanımlandığını unutmayın:

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

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

Böylece şube

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

alınacak ve *sqm->sq_containsbu işlev olan (objobjproc)tuplecontainsalınacaktır.

Bu yapar

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

... Bekle, PyObject_RichCompareBooldiğer dal bu değil miydi? Hayır, öyleydi PyObject_RichCompare.

Bu kod yolu kısaydı, bu yüzden muhtemelen bu ikisinin hızına iniyor. Hadi karşılaştıralım.

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

    ...
}

Kod yolu PyObject_RichCompareBoolhemen hemen sona erer. Çünkü PyObject_RichCompareöyle

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_LeaveRecursiveCallKombo önceki yolunda alınmadığı, ancak bu nispeten hızlı makrolardır olduğunu olacak artırma ve bazı globalsi eksiltim sonra kısa devre.

do_richcompare yapar:

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

Bu çağrıya bazı hızlı kontroller yapar v->ob_type->tp_richcompareolduğunu

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

hangisi

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

Yani, bu kısayollar left == right... ama sadece

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

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

Tüm yollarda daha sonra böyle bir şeye benziyor (bilinen dalları manuel olarak özyinelemeli olarak satır içi, açma ve budama)

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

Şimdi PyUnicode_CheckvePyUnicode_READY sadece birkaç alanı kontrol ettikleri için oldukça ucuz, ancak en üstteki kodun daha küçük bir kod yolu olduğu, daha az fonksiyon çağrısı, sadece bir anahtar ifadesi ve sadece biraz daha ince olduğu açık olmalıdır.

TL; DR:

Her ikisi de if (left_pointer == right_pointer); fark, oraya ulaşmak için ne kadar iş yaptıklarıdır. insadece daha azını yapar.


18
Bu inanılmaz bir cevap. Python projesiyle ilişkiniz nedir?
kdbanman

9
@kdbanman Yok, gerçekten, her ne kadar biraz yolumu zorlamayı başardım ;).
Veedrac

21
@varepsilon Aww, ama o zaman kimse asıl gönderiyi gözden kaçırmaz! Söz noktası gerçekten cevabı ancak kullanılan süreç değildir olsun umarım üretimde bu kesmek kullanarak insanların bir ton olacaksa değildir - cevap!
Veedrac

181

Burada, bu şaşırtıcı davranışı birleştiren üç faktör var.

İlk olarak: inoperatör bir kısayol alır ve x is yeşitliği ( x == y) kontrol etmeden önce kimliği ( ) kontrol eder :

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

İkincisi: Python'un dizesinin staj çünkü hem "x"s "x" in ("x", )aynı olacaktır:

>>> "x" is "x"
True

(büyük uyarı: Bu uygulama özgü bir davranıştır! isgerektiğini asla çünkü dizeleri karşılaştırmak için kullanılabilir olacak bazen şaşırtıcı cevaplar vermek, örneğin "x" * 100 is "x" * 100 ==> False)

Üçüncüsü: ayrıntılı olarak Veedrac fantastik cevap , tuple.__contains__( x in (y, )olup kabaca eşdeğer (y, ).__contains__(x)daha hızlı kimlik kontrolü gerçekleştiren noktasına alır) str.__eq__(yine x == yolduğu kabaca eşdeğer x.__eq__(y)yapar).

Bunun kanıtını görebilirsiniz x in (y, ), çünkü mantıksal olarak eşdeğerden önemli ölçüde yavaştır 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, )Sonra, çünkü durum yavaş iskarşılaştırma başarısız, inoperatör (yani kullanarak normal eşitlik denetimi düşer ==) karşılaştırma aynı süre olarak sürer böylece,== çünkü tuple yaratma yükü yavaş bütün operasyonu render , üyelerini yürümek vb.

Bunun yalnızcaa in (b, ) şu durumlarda daha hızlı olduğuna dikkat edin :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

(neden daha a in (b, )hızlı a is b or a == b? Tahminim daha az sanal makine talimatları olurdu -  a in (b, )sadece ~ 3 talimatları, neredea is b or a == b birkaç VM talimatları daha olacak)

Veedrac cevabı - https://stackoverflow.com/a/28889838/71522 - Her sırasında özellikle ne üzerinde çok daha ayrıntılı anlatır ==ve inve okuma değer.


3
Ve nedeni buna izin muhtemeldir yapar X in [X,Y,Z]olmadan düzgün çalışması için X, Yya da Zeşitlik yöntemlerini tanımlamak zorunda (ya da daha doğrusu, varsayılan eşitlik olduğunu iso aramak zorunda kaydeder, böylece __eq__hiçbir kullanıcı tanımlı olan nesneler üzerinde __eq__ve issadık olmak anlamına gerektiğini değeri -equality).
15'te aruisdante

1
Kullanımı float('nan')potansiyel yanıltıcıdır. Kendisine naneşit olmadığı bir özelliktir . Yani olabilir zamanlamasını değiştirin.
dawg

@dawg ah, iyi bir nokta - nan örneği sadece inüyelik testlerinde yapılan kısayol örneklerini göstermekti . Açıklığa kavuşturmak için değişken adını değiştireceğim.
David Wolever

3
Anladığım kadarıyla, CPython 3.4.3'te hangi çağrılar tuple.__contains__tarafından uygulanır ve kimlik durumunda hemen geri döner. sahip kimlik aynı kısayol sahip başlık altında. tuplecontainsPyObject_RichCompareBoolunicodePyUnicode_RichCompare
Cristian Ciupitu

3
Bu "x" is "x"mutlaka olmayacak demektir True. 'x' in ('x', )her zaman olacak True, ancak daha hızlı görünmeyebilir ==.
David Wolever
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.