Сравнение скорости сравнения гипотез Haskell Collatz - программирование
Подтвердить что ты не робот

Сравнение скорости сравнения гипотез Haskell Collatz

Мой первый опыт программирования был с Haskell. Для моих специальных потребностей мне нужен был инструмент, который был легко освоен, быстр для кодирования и прост в обслуживании, и я могу сказать, что он отлично справился с работой.

Однако в какой-то момент масштаб моих задач стал намного больше, и я подумал, что C может подойти им лучше, и это произошло. Может быть, я не был достаточно опытным с точки зрения [любого] программирования, но я не смог сделать Haskell столь же быстрым, как C, хотя я слышал, что правильный Haskell способен к подобной производительности.

Недавно я подумал, что еще раз попробую Haskell, и хотя он по-прежнему хорош для простых простых (с точки зрения вычисления) задач, он, похоже, не способен сопоставлять скорость C с такими проблемами, как гипотеза Collatz. Я читал:

Сравнение скорости с Project Euler: C против Python против Erlang vs Haskell

Оптимизация GHC: гипотеза Collatz

реализация collatz-list с использованием haskell

Но из того, что я вижу, простые методы оптимизации, в том числе:

  • выбор "более жестких" типов, таких как Int64 вместо Integer
  • оптимизация GHC на
  • с использованием простых методов оптимизации, таких как избегание ненужных вычислений или более простых функций.

все еще не делают код Haskell даже близким к почти идентичному (с точки зрения методологии) коду C для действительно больших чисел. Единственное, что, по-видимому, делает его производительность сопоставимой с C [для крупномасштабных задач], использует методы оптимизации, которые делают код длинным, ужасающим монадическим адом, что противоречит принципам, которые так сильно ценят Haskell (и I).

Здесь версия C:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int32_t col(int64_t n);

int main(int argc, char **argv)
{
    int64_t n = atoi(argv[1]), i;
    int32_t s, max;

    for(i = 2, max = 0; i <= n; ++i)
    {
        s = col(i);
        if(s > max) max = s;
    }
    printf("%d\n", max);

    return 0;
}

int32_t col(int64_t n)
{
    int32_t s;

    for(s = 0; ; ++s)
    {
        if(n == 1) break;
        n = n % 2 ? 3 * n + 1 : n / 2;
    }

    return s;
}

и версия Haskell:

module Main where

import System.Environment (getArgs)
import Data.Int (Int32, Int64)

main :: IO ()
main = do
    arg <- getArgs
    print $ maxCol 0 (read (head arg) :: Int64)

col :: Int64 -> Int32
col x = col' x 0

col' :: Int64 -> Int32 -> Int32
col' 1 n            = n
col' x n
    | rem x 2 == 0  = col' (quot x 2) (n + 1)
    | otherwise     = col' (3 * x + 1) (n + 1)

maxCol :: Int32 -> Int64 -> Int32
maxCol maxS 2   = maxS
maxCol maxS n
    | s > maxS  = maxCol s (n - 1)
    | otherwise = maxCol maxS (n - 1)
    where s = col n

TL; DR: Является ли код Haskell быстрым для записи и простым в обслуживании только для простых вычислений и теряет эту характеристику, когда производительность имеет решающее значение?

4b9b3361

Ответ 1

Большая проблема с вашим кодом Haskell заключается в том, что вы делите, чего нет в версии C.

Да, вы написали n % 2 и n / 2, но компилятор заменяет их смещениями и поразрядными. К сожалению, пока еще не учили GHC.

Если вы сделаете замену самостоятельно

module Main where

import System.Environment (getArgs)
import Data.Int (Int32, Int64)
import Data.Bits

main :: IO ()
main = do
    arg <- getArgs
    print $ maxCol 0 (read (head arg) :: Int64)

col :: Int64 -> Int32
col x = col' x 0

col' :: Int64 -> Int32 -> Int32
col' 1 n            = n
col' x n
    | x .&. 1 == 0  = col' (x `shiftR` 1) (n + 1)
    | otherwise     = col' (3 * x + 1) (n + 1)

maxCol :: Int32 -> Int64 -> Int32
maxCol maxS 2   = maxS
maxCol maxS n
    | s > maxS  = maxCol s (n - 1)
    | otherwise = maxCol maxS (n - 1)
    where s = col n

с 64-битным GHC вы получаете сопоставимую скорость (0.35s против C 0.32s на моем ящике для предел 1000000). Если вы компилируете с использованием LLVM-сервера, вам даже не нужно заменять % 2 и / 2 на побитовые операции, LLVM делает это для вас (но он производит более медленный код 0,4 с для исходного источника Haskell, что удивительно - обычно LLVM не хуже генератора исходного кода при оптимизации цикла).

С 32-разрядным GHC вы не получите сопоставимую скорость, поскольку с ними примитивные операции с 64-битными целыми числами реализуются через вызовы C - никогда не было достаточно спроса на быстрые 64-битные операции на 32- битные системы для их реализации в качестве примитивов; немногие люди, работающие в GHC, потратили свое время на другие, более важные вещи.

TL; DR: Является ли код Haskell быстрым для записи и простым в обслуживании только для простых вычислений и теряет эту характеристику, когда производительность имеет решающее значение?

Это зависит. У вас должно быть некоторое представление о том, какой код GHC генерируется из какого типа ввода, и вы должны избегать некоторых ловушек производительности. С некоторой практикой, довольно легко получить в пределах 2 × скорость gcc-O3 для таких задач.