A continuación se muestran 3 funciones que encuentran el último pero segundo elemento en una lista. El que usa last . initparece mucho más rápido que el resto. Parece que no puedo entender por qué.
Para las pruebas, utilicé una lista de entrada de [1..100000000](100 millones). El último se ejecuta casi instantáneamente, mientras que los otros tardan varios segundos.
-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"
-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse
-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init

initse ha optimizado para evitar "desempacar" la lista varias veces.myButLastmucho más lento? Parece que no está desempacando ninguna lista, sino solo atravesándola como loinithace la función ...[x, y]es la abreviatura de(x:(y:[])), por lo que desempaqueta los contras externos, un segundo contra y comprueba si la cola del segundoconses[]. Además, la segunda cláusula descomprimirá la lista nuevamente en(x:xs). Sí, desempacar es razonablemente eficiente, pero, por supuesto, si sucede con mucha frecuencia, eso ralentizará el proceso.initno verifica repetidamente si su argumento es una lista única o una lista vacía. Una vez que comienza la recursividad, solo se supone que el primer elemento se agregará al resultado de la llamada recursiva.myButLastautomática. Creo que es más probable que la fusión de la lista sea la culpable de la aceleración.Respuestas:
Al estudiar la velocidad y la optimización, es muy fácil obtener resultados tremendamente incorrectos . En particular, no puede decir realmente que una variante es más rápida que otra sin mencionar la versión del compilador y el modo de optimización de su configuración de evaluación comparativa. Incluso entonces, los procesadores modernos son tan sofisticados que cuentan con predictores de ramificación basados en redes neuronales, sin mencionar todo tipo de cachés, por lo que, incluso con una configuración cuidadosa, los resultados de la evaluación comparativa serán borrosos.
Habiendo dicho eso...
El benchmarking es nuestro amigo.
criteriones un paquete que proporciona herramientas avanzadas de evaluación comparativa. Rápidamente redacté un punto de referencia como este:Como puede ver, agregué la variante que coincide explícitamente en dos elementos a la vez, pero de lo contrario es el mismo código literalmente. También ejecuto los puntos de referencia a la inversa, para tener en cuenta el sesgo debido al almacenamiento en caché. Entonces, ¡corramos y veamos!
¡Parece que nuestra versión "lenta" no es lenta en absoluto! Y las complejidades de la coincidencia de patrones no agregan nada. (Una ligera aceleración que vemos entre dos ejecuciones consecutivas de
match2atribuyo a los efectos del almacenamiento en caché).Hay una manera de obtener más datos "científicos" : podemos
-ddump-simplver la forma en que el compilador ve nuestro código.La inspección de estructuras intermedias es nuestra amiga.
"Core" es un lenguaje interno de GHC. Cada archivo fuente de Haskell se simplifica a Core antes de transformarse en el gráfico funcional final para que se ejecute el sistema de tiempo de ejecución. Si miramos esta etapa intermedia, nos dirá eso
myButLastybutLast2son equivalentes. Hace falta mirar, ya que, en la etapa de cambio de nombre, todos nuestros identificadores agradables se destrozan al azar.Parece que
A1yA4son los más parecidos. Una inspección exhaustiva mostrará que, de hecho, el código se estructuraA1yA4es idéntico. EsoA2yA3son similares también es razonable ya que ambos se definen como una composición de dos funciones.Si va a examinar la
coresalida extensamente, tiene sentido suministrar también indicadores como-dsuppress-module-prefixesy-dsuppress-uniques. Hacen que sea mucho más fácil de leer.Una breve lista de nuestros enemigos también.
Entonces, ¿qué puede salir mal con la evaluación comparativa y la optimización?
ghci, diseñado para el juego interactivo y la iteración rápida, compila la fuente de Haskell con un cierto sabor de código de bytes, en lugar del ejecutable final, y evita optimizaciones costosas a favor de una recarga más rápida.Esto puede parecer triste. Pero en realidad no es lo que debería preocupar a un programador de Haskell, la mayoría de las veces. Historia real: Tengo un amigo que recientemente comenzó a aprender Haskell. Habían escrito un programa para la integración numérica, y fue muy lento. Así que nos sentamos juntos y escribimos una descripción categorial del algoritmo, con diagramas y demás. Cuando reescribieron el código para alinearlo con la descripción abstracta, mágicamente se convirtió en guepardo rápido y delgado en memoria también. Calculamos π en poco tiempo. ¿Moraleja de la historia? Estructura abstracta perfecta, y su código se optimizará solo.
fuente
ghciparece dar resultados diferentes (en términos de velocidad) en comparación con hacer un exe primero, como dijiste.