haskell - Filtro de referencia y partición
ghc criterion (1)
Estaba probando el rendimiento de la función de partition
para listas y obtuve algunos resultados extraños, creo.
Tenemos esa partition p xs == (filter p xs, filter (not . p) xs)
pero elegimos la primera implementación porque solo realiza un recorrido único sobre la lista. Sin embargo, los resultados que obtuve dicen que tal vez sea mejor usar la implementación que usa dos recorridos.
Aquí está el código mínimo que muestra lo que estoy viendo
import Criterion.Main
import System.Random
import Data.List (partition)
mypartition :: (a -> Bool) -> [a] -> ([a],[a])
mypartition p l = (filter p l, filter (not . p) l)
randList :: RandomGen g => g -> Integer -> [Integer]
randList gen 0 = []
randList gen n = x:xs
where
(x, gen'') = random gen
xs = randList gen'' (n - 1)
main = do
gen <- getStdGen
let arg10000000 = randList gen 10000000
defaultMain [
bgroup "filters -- split list in half " [
bench "partition100" $ nf (partition (>= 50)) arg10000000
, bench "mypartition100" $ nf (mypartition (>= 50)) arg10000000
]
]
Realicé las pruebas tanto con -O
como sin ellas, y las dos veces obtengo que el doble recorrido es mejor.
Estoy usando ghc-7.10.3
con criterion-1.1.1.0
Mis preguntas son:
¿Se espera esto?
¿Estoy usando Criterion correctamente? Sé que la pereza puede ser complicada y
(filter p xs, filter (not . p) xs)
solo hará dos recorridos si se usan ambos elementos de la tupla.¿Esto tiene que ver con la forma en que se manejan las listas en Haskell?
¡Muchas gracias!
No hay respuesta en blanco o negro a la pregunta. Para analizar el problema considere el siguiente código:
import Control.DeepSeq
import Data.List (partition)
import System.Environment (getArgs)
mypartition :: (a -> Bool) -> [a] -> ([a],[a])
mypartition p l = (filter p l, filter (not . p) l)
main :: IO ()
main = do
let cnt = 10000000
xs = take cnt $ concat $ repeat [1 .. 100 :: Int]
args <- getArgs
putStrLn $ unwords $ "Args:" : args
case args of
[percent, fun]
-> let p = (read percent >=)
in case fun of
"partition" -> print $ rnf $ partition p xs
"mypartition" -> print $ rnf $ mypartition p xs
"partition-ds" -> deepseq xs $ print $ rnf $ partition p xs
"mypartition-ds" -> deepseq xs $ print $ rnf $ mypartition p xs
_ -> err
_ -> err
where
err = putStrLn "Sorry, I do not understand."
No utilizo Criterion para tener un mejor control sobre el orden de evaluación. Para obtener los tiempos, uso la opción de tiempo de ejecución +RTS -s
. Los diferentes casos de prueba se ejecutan utilizando diferentes opciones de línea de comando. La primera opción de línea de comando define para qué porcentaje de los datos se mantiene el predicado. La segunda opción de línea de comando elige entre diferentes pruebas.
Las pruebas distinguen dos casos:
- Los datos se generan perezosamente (
partition
segundo argumento omypartition
). - Los datos ya están completamente evaluados en la memoria (2do argumento
partition-ds
omypartition-ds
).
El resultado de la partición siempre se evalúa de izquierda a derecha, es decir, comienza con la lista que contiene todos los elementos para los que se mantiene el predicado.
En el caso 1, la partition
tiene la ventaja de que los elementos de la primera lista resultante se descartan antes de que se produzcan todos los elementos de la lista de entrada. El caso 1 es especialmente bueno si el predicado coincide con muchos elementos, es decir, el primer argumento de la línea de comando es grande.
En el caso 2, la partition
no puede aprovechar esta ventaja, ya que todos los elementos ya están en la memoria.
Para mypartition
, en cualquier caso, todos los elementos se mantienen en la memoria después de que se evalúa la primera lista resultante, porque se necesitan nuevamente para calcular la segunda lista resultante. Por lo tanto, no hay mucha diferencia entre los dos casos.
Parece que, cuanto más memoria se usa, más difícil es la recolección de basura. Por lo tanto, la partition
es adecuada si el predicado coincide con muchos elementos y se usa la variante perezosa.
A la inversa, si el predicado no coincide con muchos elementos o todos los elementos ya están en la memoria, mypartition
funciona mejor, ya que su recursión no trata con los pares en contraste con la partition
.
La pregunta de "El patrón irrefutable no pierde memoria en la recursión, pero ¿por qué? ”Podría dar más información sobre el manejo de los pares en la recursión de la partition
.