함수형 프로그래밍으로 효율성을 향상시키는 방법은 무엇입니까?


20

최근에 Learn Your Haskell for Great Good 가이드 겪어 왔으며 실제로 다음과 같이 Project Euler Problem 5 를 해결하고 싶었습니다 .

1에서 20까지의 모든 숫자로 균등하게 나눌 수있는 가장 작은 양수는 무엇입니까?

주어진 숫자를 다음 숫자로 나눌 수 있는지 여부를 결정하는 함수를 먼저 작성하기로 결정했습니다.

divisable x = all (\y -> x `mod` y == 0)[1..20]

그런 다음 다음을 사용하여 가장 작은 것을 계산했습니다 head.

sm = head [x | x <- [1..], divisable x]

마지막으로 결과를 표시하는 행을 작성했습니다.

main = putStrLn $ show $ sm

불행히도이 작업을 완료하는 데 약 30 초가 걸렸습니다. 1에서 10까지의 숫자로 동일한 작업을 수행하면 거의 즉시 결과가 나오지만 결과는 1에서 20까지의 솔루션보다 훨씬 작습니다.

C에서 일찍 해결했으며 1 ~ 20의 결과도 거의 즉시 계산되었습니다. 이것은 내가 Haskell에 대해이 문제를 해석하는 방법을 오해하고 있다고 믿게합니다. 다른 사람들의 솔루션을 살펴보고 이것을 찾았습니다.

main = putStrLn $ show $ foldl1 lcm [1..20]

공평하게, 이것은 내장 함수를 사용하지만, 스스로 할 때 최종 결과가 왜 그렇게 느려 집니까? 튜토리얼은 Haskell 사용 방법을 알려 주지만 알고리즘을 빠른 코드로 변환하는 데 도움이되지는 않습니다.


6
나는 해결 된 오일러 문제의 많은 부분이 수학 문제를 다루는 데 필요한 pdf를 가지고 있음을 지적해야한다. pdf를 읽고 각 언어로 설명 된 알고리즘을 구현 한 다음 프로파일 링하십시오.

답변:


25

먼저 언어가 문제라고 생각하기 전에 최적화 된 바이너리가 있는지 확인해야합니다. Real Wolrd Haskell 의 프로파일 링 및 최적화 장을 읽으십시오 . 대부분의 경우 언어의 높은 수준의 특성으로 인해 적어도 일부 성능이 저하됩니다.

그러나 다른 솔루션은 내장 함수를 사용하기 때문에 더 빠르지 는 않지만 훨씬 빠른 알고리즘을 사용하기 때문에 더 빠르지 않습니다 . 숫자 집합의 최소 공통 배수를 찾으려면 몇 개의 GCD 만 찾으면됩니다. 이것을 1과 사이의 모든 숫자를 순환하는 솔루션과 비교하십시오 foldl lcm [1..20]. 30으로 시도하면 런타임 간의 차이가 훨씬 커집니다.

복잡성을 살펴보십시오. 알고리즘에는 O(ans*N)런타임이 있습니다. 여기서 ansN은 나누기 가능 여부를 확인하는 숫자입니다 (귀하의 경우 20).
다른 알고리즘은 실행 N시간을 lcm하지만 lcm(a,b) = a*b/gcd(a,b), GCD과 복잡성을 갖는다 O(log(max(a,b))). 따라서 두 번째 알고리즘은 복잡 O(N*log(ans))합니다. 어느 쪽이 더 빠른지 스스로 판단 할 수 있습니다.

요약하자면,
문제는 언어가 아니라 알고리즘입니다.

Mathematica와 같이 수학 중심의 수학 프로그램에 중점을 둔 특수 언어가 수학 중심 문제의 경우 거의 다른 어떤 것보다 빠릅니다. 그것은 매우 최적화 된 함수 라이브러리를 가지고 있으며 기능적 패러다임을 지원합니다 (필수적으로 명령형 프로그래밍도 지원합니다).


3
최근에 Haskell 프로그램의 성능 문제가 있었고 최적화를 끈 상태에서 컴파일 한 것을 깨달았습니다. 성능을 약 10 배 향상시켜 최적화를 전환했습니다. 따라서 C로 작성된 동일한 프로그램은 여전히 ​​빠르지 만 Haskell은 훨씬 느리지 않았습니다 (약 2, 3 배 느려서 더 좋은 성능이라고 생각합니다. 또한 더 이상 Haskell 코드를 개선하려고 시도하지 않았다고 생각합니다). 결론 : 프로파일 링 및 최적화는 좋은 제안입니다. +1
Giorgio

3
솔직히 말하면 처음 두 단락을 제거 할 수 있다고 생각하면 실제로 질문에 대답하지 않으며 아마도 정확하지 않을 것입니다 (확실히 용어가 빠르거나 느슨해지면 언어는 속도가 없습니다)
jk.

1
모순 된 답변을 제공하고 있습니다. 한편으로, 당신은 OP가 "아무것도 오해하지 않았다"고 주장하고, 느림은 하스켈에 내재되어 있다고 주장한다. 반면에 알고리즘의 선택이 중요하다는 것을 보여줍니다! 처음 두 단락을 건너 뛰면 답변이 훨씬 나아질 것입니다.이 두 단락은 나머지 답변과 다소 모순됩니다.
Andres F.

2
Andres F.와 jk의 의견을 수렴합니다. 처음 두 단락을 몇 문장으로 줄이기로 결정했습니다. 댓글 주셔서 감사합니다
K.Steff

5

내 첫 번째 생각은 20 이하의 모든 소수로 나눌 수있는 숫자 만 20보다 작은 숫자로 나눌 수 있다는 것입니다. 따라서 2 * 3 * 5 * 7 * 11 * 13 * 17 * 19의 배수 만 고려하면됩니다. . 이러한 솔루션은 무차별 대입 접근 방식의 수만큼 1 / 9,699,690을 검사합니다. 그러나 빠른 Haskell 솔루션은 그보다 좋습니다.

"fast Haskell"솔루션을 이해하면 foldl1을 사용하여 lcm (최소 공통 배수) 함수를 1에서 20까지의 숫자 목록에 적용합니다. 따라서 lcm 1 2를 적용하여 2를 산출합니다. 그런 다음 lcm 2 3을 산출합니다 6 그런 다음 lcm 6 4는 12를 산출합니다. 이런 식으로 lcm 함수는 답을 얻기 위해 19 번만 호출됩니다. Big O 표기법에서는 솔루션에 도달하는 O (n-1) 연산입니다.

slow-Haskell 솔루션은 1에서 솔루션까지 모든 숫자에 대해 1-20의 숫자를 거칩니다. 솔루션을 호출하면 slow-Haskell 솔루션이 O (s * n) 작업을 수행합니다. 우리는 이미 s가 9 백만을 초과한다는 것을 알고 있으므로 아마도 속도 저하를 설명 할 것입니다. 모든 단축키가 1-20의 숫자 목록을 통해 평균의 절반을 얻더라도 여전히 O (s * n / 2)입니다.

전화 head를 걸면 이러한 계산을 수행 할 수 있으므로 첫 번째 솔루션을 계산하기 위해 수행해야합니다.

고마워, 이것은 흥미로운 질문이었다. 그것은 나의 Haskell 지식을 정말로 확장시켰다. 지난 가을에 알고리즘을 연구하지 않았다면 전혀 대답 할 수 없었습니다.


실제로 2 * 3 * 5 * 7 * 11 * 13 * 17 * 19로 얻은 접근 방식은 적어도 lcm 기반 솔루션만큼 빠릅니다. 구체적으로 필요한 것은 2 ^ 4 * 3 ^ 2 * 5 * 7 * 11 * 13 * 17 * 19입니다. 2 ^ 4는 2의 최대 거듭 제곱이 2보다 작거나 같고 3 ^ 2가 가장 큰 검정력이기 때문에 3보다 작거나 같은
세미콜론

@semicolon 논의 된 다른 대안보다 확실히 빠르지 만이 방법에는 입력 매개 변수보다 작은 미리 계산 된 소수 목록도 필요합니다. 런타임에서 (보다 중요하게는 메모리 풋 프린트에서)이를 고려하면 불행히도이 접근 방식은 덜 매력적입니다.
K.Steff

@ K.Steff 당신은 나를 놀리고 있습니까? 당신은 19 초까지 소수를 컴퓨터로 만들어야합니다. 귀하의 진술은 절대적으로 제로 (Zero) 의미가 있습니다. 제 접근법의 총 실행 시간은 주요 세대에서도 매우 작습니다. 프로파일 링을 활성화하고 (Haskell에서의) 접근 방식 total time = 0.00 secs (0 ticks @ 1000 us, 1 processor)과를 얻었습니다 total alloc = 51,504 bytes. 런타임은 프로파일 러에 등록 할 수 없을 정도로 무시할 정도로 충분한 시간 (초)입니다.
세미콜론

@semicolon 댓글을 작성해야합니다. 죄송합니다. 내 진술은 N까지 모든 소수를 계산하는 숨겨진 가격과 관련이 있습니다. 순수한 에라토스테네스는 O (N * log (N) * log (log (N))) 연산과 O (N) 메모리입니다. N이 실제로 큰 경우 메모리 또는 시간이 부족한 알고리즘의 구성 요소. Atkin의 체로는 그다지 나아지지 않았기 때문에 알고리즘이 s보다 덜 매력적이라고 ​​결론을 내 렸습니다 foldl lcm [1..N].
K.Steff 2016 년

@ K.Steff 글쎄, 방금 두 알고리즘을 테스트했습니다. : 내 주요 기반 알고리즘의 경우 프로파일 러 (대한 N = 10) 준 total time = 0.04 secstotal alloc = 108,327,328 bytes. 다른 LCM 기반 알고리즘의 경우 프로파일 러는 내게 준 : total time = 0.67 secstotal alloc = 1,975,550,160 bytes. n = 1,000,000의 경우 프라임 기반 : total time = 1.21 secstotal alloc = 8,846,768,456 bytes, lcm 기반 : total time = 61.12 secstotal alloc = 200,846,380,808 bytes입니다. 다시 말해, 당신은 틀 렸습니다. 프라임 기반이 훨씬 좋습니다.
세미콜론

1

나는 처음에 답을 쓸 계획이 아니었다. 그러나 다른 사용자가 첫 커플 프라임을 곱하면 계산 비용이 많이 든다는 점을 반복해서 적용한다는 이상한 주장을들은 후에 나는 들었다 lcm. 두 알고리즘과 벤치 마크는 다음과 같습니다.

내 알고리즘 :

프라임 생성 알고리즘으로 무한의 프라임 목록을 제공합니다.

isPrime :: Int -> Bool
isPrime 1 = False
isPrime n = all ((/= 0) . mod n) (takeWhile ((<= n) . (^ 2)) primes)

toPrime :: Int -> Int
toPrime n 
    | isPrime n = n 
    | otherwise = toPrime (n + 1)

primes :: [Int]
primes = 2 : map (toPrime . (+ 1)) primes

이제 소수 목록을 사용하여 일부 결과를 계산하십시오 N.

solvePrime :: Integer -> Integer
solvePrime n = foldl' (*) 1 $ takeWhile (<= n) (fromIntegral <$> primes)

이제 다른 lcm 기반 알고리즘은 상당히 간결합니다. 주로 처음부터 프라임 생성을 구현했기 때문에 (그리고 성능이 좋지 않기 때문에 슈퍼 간결한 목록 이해 알고리즘을 사용하지 않았기 때문에) lcm단순히에서 가져 왔습니다 Prelude.

solveLcm :: Integer -> Integer
solveLcm n = foldl' (flip lcm) 1 [2 .. n]
-- Much slower without `flip` on `lcm`

이제 벤치 마크의 경우 각각에 사용 된 코드는 간단했습니다. ( -prof -fprof-auto -O2then +RTS -p)

main :: IO ()
main = print $ solvePrime n
-- OR
main = print $ solveLcm n

를 들어 n = 100,000, solvePrime:

total time = 0.04 secs
total alloc = 108,327,328 bytes

vs solveLcm:

total time = 0.12 secs
total alloc = 117,842,152 bytes

를 들어 n = 1,000,000, solvePrime:

total time = 1.21 secs
total alloc = 8,846,768,456 bytes

vs solveLcm:

total time = 9.10 secs
total alloc = 8,963,508,416 bytes

를 들어 n = 3,000,000, solvePrime:

total time = 8.99 secs
total alloc = 74,790,070,088 bytes

vs solveLcm:

total time = 86.42 secs
total alloc = 75,145,302,416 bytes

나는 그 결과가 스스로를 말한다고 생각한다.

프로파일 러는 프라임 생성이 작을수록 실행 시간의 비율이 점점 작아짐을 나타냅니다 n. 따라서 병목 현상이 아니므로 지금은 무시할 수 있습니다.

이것은 우리가 lcm한 인자가 1에서으로가는 곳과 다른 인자가 1에서으로가는 곳을 부르는 것을 실제로 비교하고 있음을 의미 n합니다 ans. *동일한 상황에서 전화를 걸고 프라임이 아닌 모든 번호를 건너 뛰는 이점이 *있습니다.

그리고 잘 알려져있다 *빠르고보다 lcm같이 lcm반복 응용 프로그램을 필요로 mod하고, mod점근 적으로 느린이다 ( O(n^2)~O(n^1.5)).

따라서 위의 결과와 간단한 알고리즘 분석을 통해 어떤 알고리즘이 더 빠른지 알 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.