Copiar una range(10**6)
lista aleatoria diez veces me lleva alrededor de 0.18 segundos: (estas son cinco ejecuciones)
0.175597017661
0.173731403198
0.178601711594
0.180330912952
0.180811964451
Copiar la lista sin mezclar diez veces me lleva alrededor de 0.05 segundos:
0.058402235973
0.0505464636856
0.0509734306934
0.0526022752744
0.0513324916184
Aquí está mi código de prueba:
from timeit import timeit
import random
a = range(10**6)
random.shuffle(a) # Remove this for the second test.
a = list(a) # Just an attempt to "normalize" the list.
for _ in range(5):
print timeit(lambda: list(a), number=10)
También intenté copiar con a[:]
, los resultados fueron similares (es decir, gran diferencia de velocidad)
¿Por qué la gran diferencia de velocidad? Conozco y entiendo la diferencia de velocidad en el famoso ¿Por qué es más rápido procesar una matriz ordenada que una matriz no ordenada? ejemplo, pero aquí mi procesamiento no tiene decisiones. Es simplemente copiar ciegamente las referencias dentro de la lista, ¿no?
Estoy usando Python 2.7.12 en Windows 10.
Editar: Probé Python 3.5.2 también ahora, los resultados fueron casi los mismos (mezclados consistentemente alrededor de 0.17 segundos, sin mezclar constantemente alrededor de 0.05 segundos). Aquí está el código para eso:
a = list(range(10**6))
random.shuffle(a)
a = list(a)
for _ in range(5):
print(timeit(lambda: list(a), number=10))
fuente
0.25
en cada iteración de cada una de las pruebas. Entonces, en mi plataforma, el orden sí importa.Respuestas:
Lo interesante es que depende del orden en el que se creen primero los números enteros . Por ejemplo, en lugar de
shuffle
crear una secuencia aleatoria conrandom.randint
:Esto es tan rápido como copiar su
list(range(10**6))
(primer y rápido ejemplo).Sin embargo, cuando barajas, tus enteros ya no están en el orden en que fueron creados, eso es lo que lo hace lento.
Un intermezzo rápido:
Py_INCREF
enlist_slice
), por lo que Python realmente necesita ir a donde está el objeto. No puede simplemente copiar la referencia.Entonces, cuando copia su lista, obtiene cada elemento de esa lista y lo coloca "como está" en la nueva lista. Cuando su siguiente elemento se creó poco después del actual, hay una buena posibilidad (¡no hay garantía!) De que se guarde junto a él en el montón.
Supongamos que cada vez que su computadora carga un elemento en la caché, también carga los elementos
x
siguientes en la memoria (localidad de la caché). ¡Entonces su computadora puede realizar el incremento de recuento de referencias parax+1
elementos en el mismo caché!Con la secuencia barajada, todavía carga los elementos siguientes en la memoria, pero estos no son los siguientes en la lista. Por lo tanto, no puede realizar el incremento del recuento de referencias sin buscar "realmente" el siguiente elemento.
TL; DR: La velocidad real depende de lo que sucedió antes de la copia: en qué orden se crearon estos elementos y en qué orden están en la lista.
Puede verificar esto mirando el
id
:Solo para mostrar un breve extracto:
Entonces, estos objetos están realmente "uno al lado del otro en el montón". Con
shuffle
ellos no son:Lo que muestra que estos no están realmente uno al lado del otro en la memoria:
Nota IMPORTANTE:
No lo he pensado yo mismo. La mayoría de la información se puede encontrar en la publicación del blog de Ricky Stewart .
Esta respuesta se basa en la implementación CPython "oficial" de Python. Los detalles en otras implementaciones (Jython, PyPy, IronPython, ...) pueden ser diferentes. Gracias @ JörgWMittag por señalar esto .
fuente
list_slice
y en la línea 453 puede ver laPy_INCREF(v);
llamada que necesita para acceder al objeto asignado al montón.a = [0] * 10**7
(de 10 ** 6 porque era demasiado inestable), que es incluso más rápido que el usoa = range(10**7)
(por un factor de aproximadamente 1,25). Claramente porque eso es incluso mejor para el almacenamiento en caché.[0,1,2,3]*((10**6) // 4)
es tan rápido comoa = [0] * 10**6
. Sin embargo, con los números enteros del 0 al 255, hay otro hecho: estos están internos, por lo que el orden de creación (dentro de su secuencia de comandos) ya no es importante, porque se crean cuando inicia Python.Cuando mezcla los elementos de la lista, tienen una localidad de referencia peor, lo que conduce a un peor rendimiento de la caché.
Podría pensar que copiar la lista solo copia las referencias, no los objetos, por lo que sus ubicaciones en el montón no deberían importar. Sin embargo, copiar aún implica acceder a cada objeto para modificar el recuento de referencias.
fuente
Como explicaron otros, no se trata solo de copiar las referencias, sino que también aumenta el recuento de referencias dentro de los objetos y, por lo tanto, se accede a los objetos y la caché juega un papel.
Aquí solo quiero agregar más experimentos. No tanto sobre barajado vs no barajado (donde acceder a un elemento puede perder el caché pero obtener los siguientes elementos en el caché para que sean golpeados). Pero sobre la repetición de elementos, donde los accesos posteriores del mismo elemento pueden afectar la caché porque el elemento todavía está en la caché.
Probando un rango normal:
Una lista del mismo tamaño pero con un solo elemento repetido una y otra vez es más rápida porque llega a la caché todo el tiempo:
Y no parece importar qué número sea:
Curiosamente, se vuelve aún más rápido cuando, en cambio, repito los mismos dos o cuatro elementos:
Supongo que a algo no le gusta que el mismo contador se incremente todo el tiempo. Tal vez alguna tubería se detenga porque cada aumento tiene que esperar el resultado del aumento anterior, pero esta es una suposición descabellada.
De todos modos, intente esto para un número aún mayor de elementos repetidos:
El resultado (la primera columna es el número de elementos diferentes, para cada uno pruebo tres veces y luego tomo el promedio):
Entonces, de aproximadamente 2.8 segundos para un solo elemento (repetido), cae a aproximadamente 2.2 segundos para 2, 4, 8, 16, ... elementos diferentes y permanece en aproximadamente 2.2 segundos hasta los cien mil. Creo que esto usa mi caché L2 (4 × 256 KB, tengo un i7-6700 ).
Luego, en unos pocos pasos, los tiempos suben a 3,5 segundos. Creo que esto usa una mezcla de mi caché L2 y mi caché L3 (8 MB) hasta que eso también se "agota".
Al final, se mantiene en unos 3,5 segundos, supongo que porque mis cachés ya no ayudan con los elementos repetidos.
fuente
Antes de la reproducción aleatoria, cuando se asignan en el montón, los objetos de índice adyacentes son adyacentes en la memoria y la tasa de aciertos de la memoria es alta cuando se accede a ellos; después de barajar, el objeto del índice adyacente de la nueva lista no está en la memoria. Adyacente, la tasa de aciertos es muy baja.
fuente