Ответ 1
Производительность этой программы зависит от нескольких факторов. Если мы получим все правильно, производительность будет такой же, как и в программе C. Просматривая эти факторы:
1. Использование и сравнение правильных размеров слова
Отправленный фрагмент кода C не совсем прав; он использует 32-разрядные целые числа на всех архитектурах, а Haskell Int
-s - 64 бит на 64-битной машине. Прежде всего, мы должны обязательно использовать один и тот же размер слова в обеих программах.
Кроме того, мы всегда должны использовать встроенные интегральные типы в нашем коде Haskell. Поэтому, если мы находимся на 64-битной системе, мы должны использовать 64-разрядные номера и избегать Int32
-s и Word32
-s, если только для них не существует конкретной потребности. Это связано с тем, что операции с неродными целыми числами в основном реализуются как внешние вызовы, а не примитивы, поэтому они значительно медленнее.
2. Подраздел в collatzNext
div
медленнее, чем quot
для Int
, потому что div
обрабатывает отрицательные числа по-другому. Если мы используем div
и переключаемся на Word
, программа становится быстрее, потому что div
совпадает с quot
для Word
. quot
с Int
работает так же хорошо. Тем не менее, это все еще не так быстро, как C. Мы можем разделить на два путем смещения битов вправо. По какой-то причине даже LLVM не делает это конкретное сокращение силы в этом примере, поэтому мы лучше сделаем это вручную, заменив quot n 2
на shiftR n 1
.
3. Проверка равномерности
Самый быстрый способ проверить это - проверить младший значащий бит. LLVM может оптимизировать even
к этому, в то время как родной код не может. Итак, если мы находимся на родном кодегене, even n
может быть заменен на n .&. 1 == 0
, и это дает хороший прирост производительности.
Однако, я нашел что-то вроде ошибки производительности с GHC 7.10. Здесь мы не получаем встроенный even
для Word
, который разрушает производительность (вызов функции с выделенным кучей Word
в самой горячей части кода делает это). Поэтому здесь мы должны использовать rem n 2 == 0
или n .&. 1 == 0
вместо even
. Тем не менее, even
для Int
становится тонким.
4. Слияние списков в collatzLen
Это решающий фактор. Связанный пост в блоге немного устарел относительно этого. GHC 7.8 не может слить здесь, но 7.10 может. Это означает, что с GHC 7.10 и LLVM мы можем удобно получить C-подобную производительность без существенного изменения исходного кода.
collatzNext a = (if even a then a else 3*a+1) `quot` 2
collatzLen a0 = length $ takeWhile (/= 1) $ iterate collatzNext a0
maxColLen n = maximum $ map collatzLen [1..n]
main = do
[n] <- getArgs
print $ maxColLen (read n :: Int)
С ghc-7.10.1 -O2 -fllvm
и n = 10000000
указанная выше программа работает в 2,8 секунды, а эквивалентная программа C работает в 2,4 секунды. Если я скомпилирую один и тот же код без LLVM, вместо этого я получаю 12.4 вторую рабочую среду. Это замедление полностью связано с отсутствием оптимизации на even
. Если мы используем a .&. 1 == 0
, то замедление исчезает.
5. Слияние списков при вычислении максимальной длины
Даже GHC 7.10 не может этого сделать, поэтому мы должны прибегнуть к ручному написанию.
collatzNext a = (if a .&. 1 == 0 then a else 3*a+1) `shiftR` 1
collatzLen = length . takeWhile (/= 1) . iterate collatzNext
maxCol :: Int -> Int
maxCol = go 1 1 where
go ml i n | i > n = ml
go ml i n = go (max ml (collatzLen i)) (i + 1) n
main = do
[n] <- getArgs
print $ maxCol (read n :: Int)
Теперь, для ghc-7.10.1 -O2 -fllvm
и n = 10000000
, приведенный выше код работает в 2.1 секунд, а программа C работает в 2,4 секунды. Если мы хотим добиться такой же производительности без LLVM и GHC 7.10, нам просто нужно вручную применить важные недостающие оптимизации:
collatzLen :: Int -> Int
collatzLen = go 0 where
go l 1 = l
go l n | n .&. 1 == 0 = go (l + 1) (shiftR n 1)
| otherwise = go (l + 1) (shiftR (3 * n + 1) 1)
maxCol :: Int -> Int
maxCol = go 1 1 where
go ml i n | i > n = ml
go ml i n = go (max ml (collatzLen i)) (i + 1) n
main = do
[n] <- getArgs
print $ maxCol (read n :: Int)
Теперь, с ghc-7.8.4 -O2
и n = 10000000
, наш код работает в 2.6 секунд.