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.