Почему на Haskell получается так медленно?
От: Kirikaza Россия kirikaza.ru
Дата: 21.12.11 19:04
Оценка:
Доброе время суток!

Написали мы программу, которая находит первые N простых чисел и вычисляет их сумму. Написали на C и на Haskell. Алгоритм тупой, но суть в том, что он одинаковый. В результате, программа на C выполняет задачу за 4 с, а на Haskell -- за 54 c. Из опций стоит только -O2.

Почему же такая разница? Привожу исходники:


int isPrime( int num ) {
        int count = 0;
        int i = 1;
        for( ; i <= num ; ++i ) {
                if( num % i == 0 ) ++count;
        }
        return 2 == count;
}

#define COUNT 5000

#include <stdio.h>

int main( void ) {
        int count = 0;
        int num = 1;
        int sum = 0;
        for( ; count < COUNT ; ++num ) {
                if( isPrime( num ) ) {
                        sum += num;
                        ++count;
                }
        }
        printf( "%d\n" , sum );
        return 0;
}


divisors num = filter (\i -> (mod num i) == 0 ) [1..num]

isPrime num = length (divisors num) == 2

firstPrimes count = take count (filter isPrime [1..])

count = 5000

main = print $ sum $ firstPrimes count


Впрочем, видно, что вариант на Haskell весь из себя ленивый и использует списки. Я пробовал расставлять строгость (сначала обдуманно, потом где попало) -- не помогло. Затем я подглядел вариант с параметром-аккумулятором и получился вот такой (страшный) код:


incDiv num i acc | mod num i == 0 = 1+acc
                 | otherwise      = acc

divisors num i acc | i <= num  = divisors num (i+1) (incDiv num i acc)
                   | otherwise = acc

isPrime' num = divisors num 1 0 == 2

primesSum count num acc | count > 0 = if isPrime' num
                                      then primesSum (count-1) (num+1) (num+acc)
                                      else primesSum count (num+1) acc
                        | otherwise  = acc

count = 5000

main = print (primesSum count 1 0)


Однако я получил всё те же 54 с. Подскажите, пожалуйста, что я делаю не так, и возможно ли вообще в данном случае приблизить время исполнения к сишному варианту (хотя бы в два раза медленнее, но не в 13).

--
Спасибо за внимание!
haskell slow медленный
Re: Почему на Haskell получается так медленно?
От: Буравчик Россия  
Дата: 21.12.11 19:42
Оценка:
Здравствуйте, Kirikaza, Вы писали:


Предположу, что это связано с длинной арифметикой (большими числами), которую поддерживает haskell. Чтобы избавиться от этого, надо явно прописать тип Int.

divisors :: Int -> [Int]
divisors num = filter (\i -> (mod num i) == 0 ) [1..num]

isPrime num = length (divisors num) == 2

firstPrimes count = take count (filter isPrime [1..])

count = 3000

main = print $ sum $ firstPrimes count


У меня везде получается haskell в два раза медленне, чем С. Пробовал для COUNT = 1000,2000,3000,5000.
Конфигурация: Ubuntu 10.04 LTS, GHC 7.0.3, -O2
... << RSDN@Home (RF) 1.2.0 alpha 5 rev. 17>>
Best regards, Буравчик
Re[2]: Почему на Haskell получается так медленно?
От: Буравчик Россия  
Дата: 21.12.11 19:50
Оценка: 2 (1) +1
Здравствуйте, Буравчик, Вы писали:

Лучше оставить все функции обобщенными. Поэтому более правильно было бы так:

divisors num = filter (\i -> (mod num i) == 0 ) [1..num]

isPrime num = length (divisors num) == 2

firstPrimes count = take count (filter isPrime [1..])

count = 3000

main = print $ sum (firstPrimes count :: [Int])


А вот если Int заменить на Integer (длинное целое), то время работы увеличивается в три раза.
... << RSDN@Home (RF) 1.2.0 alpha 5 rev. 17>>
Best regards, Буравчик
Re[2]: Почему на Haskell получается так медленно?
От: Kirikaza Россия kirikaza.ru
Дата: 21.12.11 20:17
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Предположу, что это связано с длинной арифметикой (большими числами), которую поддерживает haskell. Чтобы избавиться от этого, надо явно прописать тип Int.

Б>...
Б>А вот если Int заменить на Integer (длинное целое), то время работы увеличивается в три раза.

Да, действительно, после явного указания Int время работы программы Haskell снизилось с 54с до 20с, т.е. в 2.5 раза.

Хотя это всё ещё в 5 раз больше, чем вариант на C (4с).

ArchLinux 64, E8400 @3.00GHz
GHC 7.0.3, -O2
Re: Почему на Haskell получается так медленно?
От: BulatZiganshin  
Дата: 21.12.11 21:02
Оценка:
Здравствуйте, Kirikaza, Вы писали:

K>Написали мы программу, которая находит первые N простых чисел и вычисляет их сумму. Написали на C и на Haskell. Алгоритм тупой, но суть в том, что он одинаковый. В результате, программа на C выполняет задачу за 4 с, а на Haskell -- за 54 c. Из опций стоит только -O2.


имхо это на С медленно. в статьях известных хаскелистов приводятся типичные для низхкоуровневого кода случаи разницы в несколько сотен раз
Люди, я люблю вас! Будьте бдительны!!!
Re: Почему на Haskell получается так медленно?
От: Alex912  
Дата: 21.12.11 22:07
Оценка:
Здравствуйте, Kirikaza, Вы писали:

Привет,
а так сколько выходит?


import Data.List

firstPrimes ::Int->[Int]
firstPrimes  n= take n $ filter (isPrime) [2..]

count::Int->Int
count n= foldl' (+) 0 (firstPrimes  n)

isPrime::Int->Bool
isPrime x = null $ filter (\y ->  x `mod` y == 0) $ takeWhile (\y ->  y*y <= x) [2..]

main = do
        putStrLn $ (show . count) 5000
Re: Почему на Haskell получается так медленно?
От: Alex912  
Дата: 22.12.11 06:14
Оценка: 2 (1)
Здравствуйте, Kirikaza, Вы писали:


Haskell lists are ordinary single-linked lists. (Look up the term in any book on data structures.) This gives them certain speed properties which are well worth knowing.



2.1 Fast operations

The following operations are always 'fast':

Prepend 1 element (the
:
operator)
head
(get first element)
tail
(remove first element)

2.2 Slower operations
Any function that does something with the Nth element or the first N elements generally gets slower as N increases. The following all slow down as
n
gets larger:

xs !! n
take n xs
drop n xs
splitAt n xs

Any function which needs to process the entire list obviously gets slower as the list gets bigger. The following all slow down as the list
xs
gets larger:

length xs
list1 ++ list2
(speed depends only on the size of
list1
)
last xs
map my_fn xs
filter my_test xs
zip my_fn list1 list2
(speed depends on the smallest of the two lists — as does the size of the result list!)
x `elem` xs
sum xs
minimum xs
maximum xs

http://www.haskell.org/haskellwiki/How_to_work_on_lists

Поэтому думаю без takeWhile (как я поставил в коде) такой подход будет не быстрым.
Re[2]: Почему на Haskell получается так медленно?
От: samius Япония http://sams-tricks.blogspot.com
Дата: 22.12.11 06:21
Оценка:
Здравствуйте, Alex912, Вы писали:

A> Поэтому думаю без takeWhile (как я поставил в коде) такой подход будет не быстрым.


Смысл замера в том, что бы использовать одинаковый алгоритм в C и Haskell, а не в том что бы сделать его хоть сколько-нибудь эффективным.
Re[3]: Почему на Haskell получается так медленно?
От: Alex912  
Дата: 22.12.11 06:31
Оценка:
Здравствуйте, samius, Вы писали:

S>Здравствуйте, Alex912, Вы писали:


A>> Поэтому думаю без takeWhile (как я поставил в коде) такой подход будет не быстрым.


S>Смысл замера в том, что бы использовать одинаковый алгоритм в C и Haskell, а не в том что бы сделать его хоть сколько-нибудь эффективным.


Я помню была лаба на начальных курсах в университете. Посчитать фибоначчи на си через рекурсию и итерацию. В конце сделать вывод как считать лучше, как делать не стоит.
Re[4]: Почему на Haskell получается так медленно?
От: peterbes Россия  
Дата: 22.12.11 07:56
Оценка:
Здравствуйте, Alex912, Вы писали:

S>>Смысл замера в том, что бы использовать одинаковый алгоритм в C и Haskell, а не в том что бы сделать его хоть сколько-нибудь эффективным.


A>Я помню была лаба на начальных курсах в университете. Посчитать фибоначчи на си через рекурсию и итерацию. В конце сделать вывод как считать лучше, как делать не стоит.


Всё от задачи зависит. Если в теле рекурсии стоит что-то посложнее чем простое сложение, то затраты на рекурсивность нивелируются общим временем исполнения внутреннего кода.
Re[5]: Почему на Haskell получается так медленно?
От: D. Mon Великобритания http://thedeemon.livejournal.com
Дата: 22.12.11 08:00
Оценка:
Здравствуйте, peterbes, Вы писали:

P>Всё от задачи зависит. Если в теле рекурсии стоит что-то посложнее чем простое сложение, то затраты на рекурсивность нивелируются общим временем исполнения внутреннего кода.


В случае фибоначчи там просто разная сложность получается — экспоненциальная и линейная. Рекурсивно многие значения будут по несколько раз считаться.
Re[5]: Почему на Haskell получается так медленно?
От: Alex912  
Дата: 22.12.11 08:12
Оценка:
Здравствуйте, peterbes, Вы писали:

P>Здравствуйте, Alex912, Вы писали:


S>>>Смысл замера в том, что бы использовать одинаковый алгоритм в C и Haskell, а не в том что бы сделать его хоть сколько-нибудь эффективным.


A>>Я помню была лаба на начальных курсах в университете. Посчитать фибоначчи на си через рекурсию и итерацию. В конце сделать вывод как считать лучше, как делать не стоит.


P>Всё от задачи зависит. Если в теле рекурсии стоит что-то посложнее чем простое сложение, то затраты на рекурсивность нивелируются общим временем исполнения внутреннего кода.


Ну так я про это же
Re[3]: Почему на Haskell получается так медленно?
От: Klapaucius  
Дата: 22.12.11 10:07
Оценка: 13 (3)
Здравствуйте, Kirikaza, Вы писали:

K>Да, действительно, после явного указания Int время работы программы Haskell снизилось с 54с до 20с, т.е. в 2.5 раза.


K>ArchLinux 64, E8400 @3.00GHz

K>GHC 7.0.3, -O2


1) Используйте rem всесто mod. Операция % в хаскеле называется rem.
2) Используйте -fllvm.

У меня на Windows Vista x64 Q6600 @2.40GHz GHC 7.0.3 (x86) -O2 -fllvm LLVM 2.9
получается 6.25 сек. с rem и 11.69 с mod
... << RSDN@Home 1.2.0 alpha 4 rev. 1476>>
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Re[2]: Почему на Haskell получается так быстро?!
От: Kirikaza Россия kirikaza.ru
Дата: 22.12.11 19:24
Оценка: 2 (2)
Здравствуйте, Alex912, Вы писали:

A>
A>import Data.List

A>firstPrimes ::Int->[Int]
A>firstPrimes  n= take n $ filter (isPrime) [2..]

A>count::Int->Int
A>count n= foldl' (+) 0 (firstPrimes  n)

A>isPrime::Int->Bool
A>isPrime x = null $ filter (\y ->  x `mod` y == 0) $ takeWhile (\y ->  y*y <= x) [2..]

A>main = do
A>        putStrLn $ (show . count) 5000
A>


Здесь есть "нечестная" оптимизация при поиске делителей (до квадратного корня), поэтому цифра фантастическая – 0,04c.

Я заменил эту проверку на \y -> y < x – и о чудо! – удалось выжать 3.7с на Haskell против 3.9с на Си! Круто, копаю дальше.

Заменил конструкцию takeWhile (\y -> y < x) [2..] на [2..x-1]... и получил уже 1.9с!

Стало весьма интересно, откуда всё это. С foldl' я уже пробовал баловаться вместо sum – разницы не было. Проведя несколько экспериментов, выяснил, что null xs работает гораааздо быстрее, чем length xs == 0. На данном примере – в 10 раз. В принципе, можно найти этому объяснение; тем не менее в Notes_about_speed об этом ни слова.

В приводимом мной коде делители считались от 1 до самого числа включительно, поэтому длину списка делителей нужно было сравнивать с 2. Т.е. null уже не подходит... И тут пришла идея сравнивать не длину списка делителей, а сам этот список с [1,x]. И чудо повторилось: программа выполнилась за 1,9с.

Таким образом, минимальным изменением время выполнения снизилось с 20с до 1,9с:

divisors num = filter (\i -> (mod num i) == 0 ) [1..num]

-- isPrime num = length (divisors num) == 2
isPrime num = (divisors num) == [1,num]

firstPrimes count = take count (filter isPrime [1..])

count = 5000

main = print $ sum ( firstPrimes count :: [Int])


В принципе, мне так даже больше нравится, но... может кто-нибудь объяснить, в чём здесь фокус?

P.S.: После замены mod на rem время снизилось ещё – до 1,5c.
haskell speed скорость null length mod rem
Re[3]: Почему на Haskell получается так быстро?!
От: Буравчик Россия  
Дата: 22.12.11 19:42
Оценка: 7 (1)
Здравствуйте, Kirikaza, Вы писали:

K>Здравствуйте, Alex912, Вы писали:


divisors num = filter (\i -> (mod num i) == 0 ) [1..num]

-- isPrime num = length (divisors num) == 2
isPrime num = (divisors num) == [1,num]

firstPrimes count = take count (filter isPrime [1..])

count = 5000

main = print $ sum ( firstPrimes count :: [Int])


Этот код не соответствует коду на С. Дело в ленивости при вычислении выражения "(divisors num) == [1,num]".

Здесь divisors вычисляется лениво. Сначала вычисляется первый элемент divisors и сравнивается с единицей. Затем вычисляется второй элемент и сравнивается с num. И если второй элемент будет не равен num, то списки будут признаны не равными. Именно это происходит, если num — составное число (не простое).

Т.е. для простых чисел divisors проверяет все делители, а для составных чисел divisors будет работать до первого попавшегося делителя отличного от единицы, и другие делители искать не будет. А вот код на С всегда просматривает все возможные делители. Поэтому С получился медленнее.
... << RSDN@Home (RF) 1.2.0 alpha 5 rev. 17>>
Best regards, Буравчик
Re[3]: Почему на Haskell получается так быстро?!
От: Буравчик Россия  
Дата: 23.12.11 06:26
Оценка:
Здравствуйте, Kirikaza, Вы писали:

K>Стало весьма интересно, откуда всё это. С foldl' я уже пробовал баловаться вместо sum – разницы не было. Проведя несколько экспериментов, выяснил, что null xs работает гораааздо быстрее, чем length xs == 0. На данном примере – в 10 раз. В принципе, можно найти этому объяснение; тем не менее в Notes_about_speed об этом ни слова.


Для вычисления length xs требуется пройтись по всему списку xs и посчитать элементы. А для вычисления null достаточно просмотреть первый элемент xs. Если он существует, то список не пуст.

length — сложность O(n), null — сложность O(1)
Best regards, Буравчик
Re[3]: Почему на Haskell получается так быстро?!
От: VoidEx  
Дата: 23.12.11 21:03
Оценка:
Здравствуйте, Kirikaza, Вы писали:

Проверять длину списка на "меньше или равно n" надо так:

null . drop n


В этом случае не надо пробегать весь список.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.