Хотя это уже довольно старое, позвольте мне упомянуть, есть один важный момент, который ранее не рассматривался.
Во-первых, время различных программ на моем компьютере.Поскольку я работаю в 64-битной системе linux, они показывают несколько иные характеристики: использование Integer
вместо Int64
не повышает производительность, как это было бы с 32-битным GHC, где каждая операция Int64
влечет за собойстоимость C-вызова, в то время как вычисления с подгонкой Integer
в знаковых 32-разрядных целых числах не нуждаются во внешнем вызове (так как здесь только несколько операций превышают этот диапазон, Integer
- лучший выбор для 32-разрядногоGHC).
- C: 0,3 секунды
- Оригинальный Haskell: 14,24 секунды, используя
Integer
вместо Int64
: 33,96 секунды - Улучшенная версия KennyTM:5,55 секунды, используя
Int
: 1,85 секунды - версия Криса Куклевича: 5,73 секунды, используя
Int
: 1,90 секунды - версия FUZxxl: 3,56 секунды, используя
quotRem
вместо divMod
: 1,79 секунды
Так что же у нас?
- Рассчитать длину с помощью аккумулятора, чтобы компилятор мог преобразовать его (в основном) в цикл
- Не пересчитывайте последовательностьдля сравнения
- Не использовать
div
соотв.divMod
когда это не нужно, quot
соотв.quotRem
намного быстрее
Чего еще не хватает?
if (j % 2 == 0)
j = j / 2;
else
j = 3 * j + 1;
Любой использованный мной C-компилятор преобразует тест j % 2 == 0
в битовую маску и не делаетиспользуйте инструкцию деления.GHC этого не делает (пока).Поэтому тестирование even n
или вычисление n `quotRem` 2
- довольно дорогая операция.Замена nextNumber
в Integer
версии KennyTM на
nextNumber :: Integer -> Integer
nextNumber n
| fromInteger n .&. 1 == (0 :: Int) = n `quot` 2
| otherwise = 3*n+1
сокращает время его работы до 3,25 секунды (Примечание: для Integer
, n `quot` 2
быстрее, чем n `shiftR` 1
, что занимает 12,69 секунды!).
То же самое в версии Int
сокращает время работы до 0,41 секунды.Для Int
с сдвиг битов для деления на 2 немного быстрее, чем операция quot
, что сокращает его время выполнения до 0,39 секунды.
Устраняет построение списка (это непоявляется в версии C либо),
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
приводит к еще небольшому ускорению, в результате чего время работы составляет 0,37 секунды.
Таким образом, версия на Haskell близко соответствует версии Cне занимает много времени, это фактор ~ 1,3.
Ну, давайте будем честными, в C-версии есть неэффективность, которой нет в версиях на Haskell,
if (this_terms > terms)
{
terms = this_terms;
longest = i;
}
появляется во внутреннем цикле.Удаление этого из внутреннего цикла в версии C сокращает время его работы до 0,27 секунды, делая коэффициент ~ 1,4.