ylab color change categoryorder python performance python-3.x python-internals

python - color - plotly layout



¿Por qué es ''x'' en(''x'',) más rápido que ''x''== ''x''? (2)

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

También funciona para tuplas con múltiples elementos, ambas versiones parecen crecer linealmente:

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

Basado en esto, ¡creo que debería comenzar a usarlo in todas partes in lugar de == !


Aquí hay tres factores en juego que, combinados, producen este comportamiento sorprendente.

Primero: el operador in toma un atajo y verifica la identidad ( x is y ) antes de verificar la igualdad ( x == y ):

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

Segundo: debido a la secuencia interna de Python, ambas "x" s en "x" in ("x", ) serán idénticas:

>>> "x" is "x" True

(gran advertencia: ¡este es un comportamiento específico de la implementación! nunca debe usarse para comparar cadenas porque a veces dará respuestas sorprendentes; por ejemplo, "x" * 100 is "x" * 100 ==> False )

Tercero: como se detalla en la fantástica respuesta de Veedrac , la tuple.__contains__ ( x in (y, ) es aproximadamente equivalente a (y, ).__contains__(x) ) llega al punto de realizar la verificación de identidad más rápido que str.__eq__ (nuevamente, x == y es aproximadamente equivalente a x.__eq__(y) ) hace.

Puede ver evidencia de esto porque x in (y, ) es significativamente más lento que el equivalente lógico, 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

El caso x in (y, ) es más lento porque, después de que falla la comparación is , el operador in vuelve a la verificación de igualdad normal (es decir, usando == ), por lo que la comparación toma aproximadamente la misma cantidad de tiempo que == , lo que representa toda la operación es más lenta debido a la sobrecarga de crear la tupla, caminar sobre sus miembros, etc.

Tenga en cuenta también que a in (b, ) solo es más rápido cuando 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

(¿por qué es a in (b, ) más rápido que a is b or a == b ? Supongo que serían menos instrucciones de máquina virtual: a in (b, ) es solo ~ 3 instrucciones, donde a is b or a == b habrá bastantes más instrucciones de VM)

La respuesta de Veedrac - https://.com/a/28889838/71522 - entra en mucho más detalle específicamente sobre lo que sucede durante cada uno de == y in y vale la pena leerlo.


Como le mencioné a David Wolever, hay más en esto de lo que parece; ambos métodos se envían a is ; puedes probar esto haciendo

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

El primero solo puede ser tan rápido porque verifica por identidad.

Para descubrir por qué uno tomaría más tiempo que el otro, analicemos la ejecución.

Ambos comienzan en ceval.c , desde COMPARE_OP ya que ese es el bytecode involucrado

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

Esto muestra los valores de la pila (técnicamente solo muestra uno)

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

y ejecuta la comparación:

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

cmp_outcome es esto:

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

Aquí es donde se dividen los caminos. La rama PyCmp_IN hace

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

Tenga en cuenta que una tupla se define como

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

Entonces la rama

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

se tomará y *sqm->sq_contains , que es la función (objobjproc)tuplecontains , se tomará.

Esto hace

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

... Espera, ¿no fue eso PyObject_RichCompareBool lo que PyObject_RichCompareBool la otra rama? No, eso fue PyObject_RichCompare .

Esa ruta de código era corta, por lo que probablemente se reduzca a la velocidad de estos dos. Comparemos.

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

La ruta del código en PyObject_RichCompareBool casi de inmediato. Para PyObject_RichCompare , lo hace

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

El combo Py_EnterRecursiveCall / Py_LeaveRecursiveCall no se toman en la ruta anterior, pero estas son macros relativamente rápidas que cortocircuitarán después de aumentar y disminuir algunas variables globales.

do_richcompare hace:

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

Esto hace algunas comprobaciones rápidas para llamar a v->ob_type->tp_richcompare que es

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

que hace

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

A saber, estos atajos a la left == right ... pero solo después de hacer

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

Después de todo, todos los caminos se ven así (reclinando, desenrollando y podando manualmente ramas conocidas)

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

Ahora, PyUnicode_Check y PyUnicode_READY son bastante baratos ya que solo verifican un par de campos, pero debería ser obvio que el superior es una ruta de código más pequeña, tiene menos llamadas a funciones, solo una declaración de interruptor y es un poco más delgada.

TL; DR:

Ambos despachan a if (left_pointer == right_pointer) ; la diferencia es cuánto trabajo hacen para llegar allí. in solo hace menos.