para pagina online oficial descargar compiler performance haskell optimization recursion benchmarking

performance - pagina - Ackermann muy ineficiente con Haskell/GHC



haskell pagina oficial (7)

Intento calcular Ackermann(4,1) y hay una gran diferencia en el rendimiento entre diferentes lenguajes / compiladores. Debajo están los resultados en mi Core i7 3820QM, 16G, Ubuntu 12.10 64bit ,

C: 1.6s , gcc -O3 (con gcc 4.7.2)

int ack(int m, int n) { if (m == 0) return n+1; if (n == 0) return ack(m-1, 1); return ack(m-1, ack(m, n-1)); } int main() { printf("%d/n", ack(4,1)); return 0; }

OCaml: 3.6s , ocamlopt (con ocaml 3.12.1)

let rec ack = function | 0,n -> n+1 | m,0 -> ack (m-1, 1) | m,n -> ack (m-1, ack (m, n-1)) in print_int (ack (4, 1))

Estándar ML: 5.1s mlton -codegen c -cc-opt -O3 (con mlton 20100608)

fun ack 0 n = n+1 | ack m 0 = ack (m-1) 1 | ack m n = ack (m-1) (ack m (n-1)); print (Int.toString (ack 4 1));

Raqueta: 11.5s racket (con raqueta v5.3.3)

(require racket/unsafe/ops) (define + unsafe-fx+) (define - unsafe-fx-) (define (ack m n) (cond [(zero? m) (+ n 1)] [(zero? n) (ack (- m 1) 1)] [else (ack (- m 1) (ack m (- n 1)))])) (time (ack 4 1))

Haskell: inacabado , muerto por sistema después de 22 s ghc -O2 (con ghc 7.4.2)

Haskell: 1.8 ajhc (con ajhc 0.8.0.4)

main = print $ ack 4 1 where ack :: Int -> Int -> Int ack 0 n = n+1 ack m 0 = ack (m-1) 1 ack m n = ack (m-1) (ack m (n-1))

La versión de Haskell es la única que no finaliza correctamente porque requiere demasiada memoria. Congela mi máquina y llena el espacio de intercambio antes de ser asesinado. ¿Qué puedo hacer para mejorarlo sin exagerar el código?

EDITAR : Aprecio algunas de las soluciones asintóticamente más inteligentes, pero no son exactamente lo que estoy pidiendo. Se trata más bien de ver si el compilador maneja ciertos patrones de una manera razonablemente eficiente (pila, llamadas de cola, unboxing, etc.) que la computación de la función ackermann.

EDIT 2 : Como se señala en varias respuestas, esto parece ser un error en las versiones recientes de GHC . Intento el mismo código con AJHC y obtengo un rendimiento mucho mejor.

Muchas gracias :)


Escribir el algoritmo en Haskell de una manera similar a la forma en que lo escribió en C no es el mismo algoritmo, porque la semántica de la recursión es bastante diferente.

Aquí hay una versión que usa el mismo algoritmo matemático , pero donde representamos llamadas a la función de Ackermann simbólicamente usando un tipo de datos. De esta forma, podemos controlar la semántica de la recursión de manera más precisa.

Cuando se compila con optimización, esta versión se ejecuta en memoria constante, pero es lenta, unos 4,5 minutos en un entorno similar al suyo. Pero estoy seguro de que podría modificarse para ser mucho más rápido. Esto es solo para dar la idea.

data Ack = Ack !Int ack :: Int -> Int -> Int ack m n = length . ackR $ Ack m : replicate n (Ack 0) where ackR n@(Ack 0 : _) = n ackR n = ackR $ ack'' n ack'' [] = [] ack'' (Ack 0 : n) = Ack 0 : ack'' n ack'' [Ack m] = [Ack (m-1), Ack 0] ack'' (Ack m : n) = Ack (m-1) : ack'' (Ack m : decr n) decr (Ack 0 : n) = n decr n = decr $ ack'' n


Esta versión usa algunas propiedades de la función ackermann. No es equivalente a las otras versiones, pero es rápido:

ackermann :: Int -> Int -> Int ackermann 0 n = n + 1 ackermann m 0 = ackermann (m - 1) 1 ackermann 1 n = n + 2 ackermann 2 n = 2 * n + 3 ackermann 3 n = 2 ^ (n + 3) - 3 ackermann m n = ackermann (m - 1) (ackermann m (n - 1))

Editar: Y esta es una versión con memoria, vemos que es fácil memorizar una función en haskell, el único cambio está en el sitio de llamadas:

import Data.Function.Memoize ackermann :: Integer -> Integer -> Integer ackermann 0 n = n + 1 ackermann m 0 = ackermann (m - 1) 1 ackermann 1 n = n + 2 ackermann 2 n = 2 * n + 3 ackermann 3 n = 2 ^ (n + 3) - 3 ackermann m n = ackermann (m - 1) (ackermann m (n - 1)) main :: IO () main = print $ memoize2 ackermann 4 2


Este problema de rendimiento (a excepción del error GHC RTS obviamente) parece estar solucionado ahora en OS X 10.8 después de que Apple XCode actualice a 4.6.2 . Todavía puedo reproducirlo en Linux (he estado probando con el backend de GHC LLVM), pero ya no en OS X. Después de actualizar el XCode a 4.6.2, la nueva versión parece haber afectado la generación de código backend de GHC para Ackermann sustancialmente (por lo que recuerdo al mirar depósitos de objetos previos a la actualización). Pude reproducir el problema de rendimiento en Mac antes de la actualización de XCode. No tengo los números, pero seguramente fueron bastante malos. Por lo tanto, parece que la actualización de XCode mejoró la generación de código GHC para Ackermann.

Ahora, las versiones C y GHC están bastante cerca. Código C:

int ack(int m,int n){ if(m==0) return n+1; if(n==0) return ack(m-1,1); return ack(m-1, ack(m,n-1)); }

Hora de ejecutar ack (4,1):

GCC 4.8.0: 2.94s Clang 4.1: 4s

Código Haskell:

ack :: Int -> Int -> Int ack 0 n = n+1 ack m 0 = ack (m-1) 1 ack m n = ack (m-1) (ack m (n-1))

Hora de ejecutar ack 4 1 (con + RTS -kc1M):

GHC 7.6.1 Native: 3.191s GHC 7.6.1 LLVM: 3.8s

Todos fueron compilados con -O2 bandera (y -rtsopts bandera para GHC para solución de error RTS). Aunque es bastante un rascador de cabeza. La actualización de XCode parece haber marcado una gran diferencia con la optimización de Ackermann en GHC.


La siguiente es una versión idiomática que aprovecha la lazyness de Haskell y la optimización de GHC de las expresiones constantes de alto nivel.

acks :: [[Int]] acks = [ [ case (m, n) of (0, _) -> n + 1 (_, 0) -> acks !! (m - 1) !! 1 (_, _) -> acks !! (m - 1) !! (acks !! m !! (n - 1)) | n <- [0..] ] | m <- [0..] ] main :: IO () main = print $ acks !! 4 !! 1

Aquí, estamos construyendo perezosamente una matriz para todos los valores de la función de Ackermann. Como resultado, las llamadas subsiguientes a acks no volverán a calcular nada (es decir, evaluar acks !! 4 !! 1 nuevo no duplicará el tiempo de ejecución).

Aunque esta no es la solución más rápida, se parece mucho a la implementación ingenua, es muy eficiente en términos de uso de la memoria, y reescribe una de las características más extrañas de Haskell (pereza) como una fortaleza.


No veo que esto sea un error en absoluto, ghc simplemente no está aprovechando el hecho de que sabe que 4 y 1 son los únicos argumentos con los que se llamará a la función, es decir, para decirlo sin rodeos , no hace trampa. Tampoco hace matemática constante para usted, así que si hubiera escrito main = print $ ack (2+2) 1 , no habría calculado que 2 + 2 = 4 hasta el tiempo de ejecución. El ghc tiene cosas mucho más importantes en que pensar. La ayuda para la última dificultad está disponible si te interesa http://hackage.haskell.org/package/const-math-ghc-plugin .

Así que se ayuda a ghc si se hace un poco de matemática, por ejemplo, esto es por lo menos un tiempo cazado tan rápido como su programa C con 4 y 1 como argumentos. Pero pruébalo con 4 y 2:

main = print $ ack 4 2 where ack :: Int -> Integer -> Integer ack 0 n = n + 1 ack 1 n = n + 2 ack 2 n = 2 * n + 3 ack m 0 = ack (m-1) 1 ack m n = ack (m-1) (ack m (n-1) )

Esto dará la respuesta correcta , todos ~ 20,000 dígitos, en menos de una décima de segundo, mientras que el gcc, con su algoritmo, demorará una eternidad para dar la respuesta incorrecta.


Parece que hay algún tipo de error involucrado. ¿Qué versión de GHC estás usando?

Con GHC 7, tengo el mismo comportamiento que tú. El programa consume toda la memoria disponible sin producir ningún resultado.

Sin embargo, si lo compilo con GHC 6.12.1 solo con ghc --make -O2 Ack.hs , funciona perfectamente. Calcula el resultado en 10.8s en mi computadora, mientras que la versión C simple toma 7.8s .

Le sugiero que informe este error en el sitio web de GHC .


NB: El problema de uso de memoria alta es un error en el GHC RTS , donde al desbordamiento de la pila y la asignación de nuevas pilas en el montón no se verificaba si la recolección de basura se debe. Ya se ha corregido en GHC HEAD.

Pude obtener un rendimiento mucho mejor con el ack conversión de CPS:

module Main where data P = P !Int !Int main :: IO () main = print $ ack (P 4 1) id where ack :: P -> (Int -> Int) -> Int ack (P 0 n) k = k (n + 1) ack (P m 0) k = ack (P (m-1) 1) k ack (P m n) k = ack (P m (n-1)) (/a -> ack (P (m-1) a) k)

Su función original consume toda la memoria disponible en mi máquina, mientras que esta se ejecuta en un espacio constante.

$ time ./Test 65533 ./Test 52,47s user 0,50s system 96% cpu 54,797 total

Ocaml es aún más rápido, sin embargo:

$ time ./test 65533./test 7,97s user 0,05s system 94% cpu 8,475 total

Editar: cuando se compila con JHC , su programa original es tan rápido como la versión de Ocaml:

$ time ./hs.out 65533 ./hs.out 5,31s user 0,03s system 96% cpu 5,515 total

Edición 2: Algo más que descubrí: ejecutar tu programa original con un tamaño de pila más grande ( +RTS -kc1M ) lo hace funcionar en un espacio constante. La versión de CPS es aún un poco más rápida.

Edición 3: Logré producir una versión que se ejecuta casi tan rápido como el Ocaml desenrollando manualmente el lazo principal. Sin embargo, solo funciona cuando se ejecuta con +RTS -kc1M (Dan Doel ha presentado un error sobre este comportamiento):

{-# LANGUAGE CPP #-} module Main where data P = P {-# UNPACK #-} !Int {-# UNPACK #-} !Int ack0 :: Int -> Int ack0 n =(n+1) #define C(a) a #define CONCAT(a,b) C(a)C(b) #define AckType(M) CONCAT(ack,M) :: Int -> Int AckType(1) AckType(2) AckType(3) AckType(4) #define AckDecl(M,M1) / CONCAT(ack,M) n = case n of { 0 -> CONCAT(ack,M1) 1 / ; 1 -> CONCAT(ack,M1) (CONCAT(ack,M1) 1) / ; _ -> CONCAT(ack,M1) (CONCAT(ack,M) (n-1)) } AckDecl(1,0) AckDecl(2,1) AckDecl(3,2) AckDecl(4,3) ack :: P -> (Int -> Int) -> Int ack (P m n) k = case m of 0 -> k (ack0 n) 1 -> k (ack1 n) 2 -> k (ack2 n) 3 -> k (ack3 n) 4 -> k (ack4 n) _ -> case n of 0 -> ack (P (m-1) 1) k 1 -> ack (P (m-1) 1) (/a -> ack (P (m-1) a) k) _ -> ack (P m (n-1)) (/a -> ack (P (m-1) a) k) main :: IO () main = print $ ack (P 4 1) id

Pruebas:

$ time ./Test +RTS -kc1M 65533 ./Test +RTS -kc1M 6,30s user 0,04s system 97% cpu 6,516 total

Editar 4 : Aparentemente, la fuga de espacio se soluciona en GHC HEAD , por lo que +RTS -kc1M no será necesario en el futuro.