Aunque esto ya es bastante antiguo, permítanme decirlo, hay un punto crucial que no se ha abordado anteriormente.
Primero, los tiempos de los diferentes programas en mi caja. Dado que estoy en un sistema Linux de 64 bits, muestran características algo diferentes: usar Integer
en lugar de Int64
no mejora el rendimiento como lo haría con un GHC de 32 bits, donde cada operación Int64
incurriría en el costo de una llamada C mientras que los cálculos con Integer
s ajustados en enteros de 32 bits con signo no necesitan una llamada externa (dado que solo unas pocas operaciones exceden ese rango aquí, Integer
es la mejor opción en un GHC de 32 bits).
- C: 0,3 segundos
- original Haskell: 14,24 segundos, utilizando
Integer
en lugar de Int64
: 33.96 segundos
- versión mejorada de KennyTM: 5.55 segundos, utilizando
Int
: 1,85 segundos
- versión de Chris Kuklewicz: 5,73 segundos, usando
Int
: 1.90 segundos
- Versión de FUZxxl: 3.56 segundos, usando
quotRem
en lugar de divMod
: 1.79 segundos
¿Qué tenemos?
- calcular la longitud de un acumulador por lo que el compilador puede transformarlo (básicamente) en un bucle
- No volver a calcular la secuencia de longitudes para las comparaciones
- No utilice
div
resp. divMod
cuando no es necesario, quot
resp. quotRem
son mucho más rápidos
¿Qué falta?
if (j % 2 == 0)
j = j/2;
else
j = 3 * j + 1;
Cualquier compilador de C He utilizado transforma la prueba j % 2 == 0
en un bit de enmascaramiento y no utiliza una instrucción de división. GHC no (todavía) hace eso.Entonces, probar even n
o computar n `quotRem` 2
es una operación bastante costosa. Sustitución nextNumber
en la versión de KennyTM Integer
con
nextNumber :: Integer -> Integer
nextNumber n
| fromInteger n .&. 1 == (0 :: Int) = n `quot` 2
| otherwise = 3*n+1
reduce su tiempo de ejecución a 3,25 segundos (Nota: para Integer
, n `quot` 2
es más rápido que n `shiftR` 1
, que se lleva a 12.69 segundos).
Haciendo lo mismo en la versión Int
reduce su tiempo de ejecución a 0,41 segundos. Para Int
s, el cambio de bit para división por 2 es un poco más rápido que la operación quot
, reduciendo su tiempo de ejecución a 0.39 segundos.
La eliminación de la construcción de la lista (que no aparece en la versión C tampoco),
module Main (main) where
import Data.Bits
result :: Int
result = findMax 0 0 1
findMax :: Int -> Int -> Int -> Int
findMax start len can
| can > 1000000 = start
| canlen > len = findMax can canlen (can+1)
| otherwise = findMax start len (can+1)
where
canlen = findLen 1 can
findLen :: Int -> Int -> Int
findLen l 1 = l
findLen l n
| n .&. 1 == 0 = findLen (l+1) (n `shiftR` 1)
| otherwise = findLen (l+1) (3*n+1)
main :: IO()
main = print result
produce un pequeño aumento de velocidad aún más, resultando en un tiempo de funcionamiento de 0,37 segundos.
Así que la versión de Haskell que está en estrecha correspondencia con la versión C no tarda mucho más, es un factor de ~ 1.3.
Bueno, seamos justos, hay una ineficiencia en la versión C que no está presente en las versiones de Haskell,
if (this_terms > terms)
{
terms = this_terms;
longest = i;
}
que aparece en el bucle interno. Al levantarlo del lazo interno en la versión C, se reduce su tiempo de funcionamiento a 0,27 segundos, lo que hace que el factor sea ~ 1,4.
Su 'unsigned long' puede tener solo 32 bits de longitud. Para una comparación justa, use un 'unsigned long long' o' uint64_t'. – kennytm
@KennyTM - punto justo - Estaba probando en Ubuntu de 32 bits donde un largo pasa a ser de 64 bits. – stusmith
@stusmith: Ya veo. Eso está bien, entonces. – kennytm