이 Haskell 코드가 -O를 사용하면 왜 느리게 실행됩니까?


87

이 Haskell 코드는에서 훨씬 느리게 실행 -O되지만 위험하지-O 않아야합니다 . 누가 무슨 일이 있었는지 말해 줄 수 있습니까? 중요한 경우이 문제 를 해결하려는 시도 이며 이진 검색 및 영구 세그먼트 트리를 사용합니다.

import Control.Monad
import Data.Array

data Node =
      Leaf   Int           -- value
    | Branch Int Node Node -- sum, left child, right child
type NodeArray = Array Int Node

-- create an empty node with range [l, r)
create :: Int -> Int -> Node
create l r
    | l + 1 == r = Leaf 0
    | otherwise  = Branch 0 (create l m) (create m r)
    where m = (l + r) `div` 2

-- Get the sum in range [0, r). The range of the node is [nl, nr)
sumof :: Node -> Int -> Int -> Int -> Int
sumof (Leaf val) r nl nr
    | nr <= r   = val
    | otherwise = 0
sumof (Branch sum lc rc) r nl nr
    | nr <= r   = sum
    | r  > nl   = (sumof lc r nl m) + (sumof rc r m nr)
    | otherwise = 0
    where m = (nl + nr) `div` 2

-- Increase the value at x by 1. The range of the node is [nl, nr)
increase :: Node -> Int -> Int -> Int -> Node
increase (Leaf val) x nl nr = Leaf (val + 1)
increase (Branch sum lc rc) x nl nr
    | x < m     = Branch (sum + 1) (increase lc x nl m) rc
    | otherwise = Branch (sum + 1) lc (increase rc x m nr)
    where m = (nl + nr) `div` 2

-- signature said it all
tonodes :: Int -> [Int] -> [Node]
tonodes n = reverse . tonodes' . reverse
    where
        tonodes' :: [Int] -> [Node]
        tonodes' (h:t) = increase h' h 0 n : s' where s'@(h':_) = tonodes' t
        tonodes' _ = [create 0 n]

-- find the minimum m in [l, r] such that (predicate m) is True
binarysearch :: (Int -> Bool) -> Int -> Int -> Int
binarysearch predicate l r
    | l == r      = r
    | predicate m = binarysearch predicate l m
    | otherwise   = binarysearch predicate (m+1) r
    where m = (l + r) `div` 2

-- main, literally
main :: IO ()
main = do
    [n, m] <- fmap (map read . words) getLine
    nodes <- fmap (listArray (0, n) . tonodes n . map (subtract 1) . map read . words) getLine
    replicateM_ m $ query n nodes
    where
        query :: Int -> NodeArray -> IO ()
        query n nodes = do
            [p, k] <- fmap (map read . words) getLine
            print $ binarysearch (ok nodes n p k) 0 n
            where
                ok :: NodeArray -> Int -> Int -> Int -> Int -> Bool
                ok nodes n p k s = (sumof (nodes ! min (p + s + 1) n) s 0 n) - (sumof (nodes ! max (p - s) 0) s 0 n) >= k

(이것은 코드 리뷰 와 정확히 동일한 코드 이지만이 질문은 다른 문제를 해결합니다.)

이것은 C ++의 입력 생성기입니다.

#include <cstdio>
#include <cstdlib>
using namespace std;
int main (int argc, char * argv[]) {
    srand(1827);
    int n = 100000;
    if(argc > 1)
        sscanf(argv[1], "%d", &n);
    printf("%d %d\n", n, n);
    for(int i = 0; i < n; i++)
        printf("%d%c", rand() % n + 1, i == n - 1 ? '\n' : ' ');
    for(int i = 0; i < n; i++) {
        int p = rand() % n;
        int k = rand() % n + 1;
        printf("%d %d\n", p, k);
    }
}

만일 당신이이 C ++ 컴파일러를 사용할 필요가 없습니다, 이것은의 결과입니다./gen.exe 1000 .

이것은 내 컴퓨터의 실행 결과입니다.

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.8.3
$ ghc -fforce-recomp 1827.hs
[1 of 1] Compiling Main             ( 1827.hs, 1827.o )
Linking 1827.exe ...
$ time ./gen.exe 1000 | ./1827.exe > /dev/null
real    0m0.088s
user    0m0.015s
sys     0m0.015s
$ ghc -fforce-recomp -O 1827.hs
[1 of 1] Compiling Main             ( 1827.hs, 1827.o )
Linking 1827.exe ...
$ time ./gen.exe 1000 | ./1827.exe > /dev/null
real    0m2.969s
user    0m0.000s
sys     0m0.045s

다음은 힙 프로필 요약입니다.

$ ghc -fforce-recomp -rtsopts ./1827.hs
[1 of 1] Compiling Main             ( 1827.hs, 1827.o )
Linking 1827.exe ...
$ ./gen.exe 1000 | ./1827.exe +RTS -s > /dev/null
      70,207,096 bytes allocated in the heap
       2,112,416 bytes copied during GC
         613,368 bytes maximum residency (3 sample(s))
          28,816 bytes maximum slop
               3 MB total memory in use (0 MB lost due to fragmentation)
                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0       132 colls,     0 par    0.00s    0.00s     0.0000s    0.0004s
  Gen  1         3 colls,     0 par    0.00s    0.00s     0.0006s    0.0010s
  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    0.03s  (  0.03s elapsed)
  GC      time    0.00s  (  0.01s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time    0.03s  (  0.04s elapsed)
  %GC     time       0.0%  (14.7% elapsed)
  Alloc rate    2,250,213,011 bytes per MUT second
  Productivity 100.0% of total user, 83.1% of total elapsed
$ ghc -fforce-recomp -O -rtsopts ./1827.hs
[1 of 1] Compiling Main             ( 1827.hs, 1827.o )
Linking 1827.exe ...
$ ./gen.exe 1000 | ./1827.exe +RTS -s > /dev/null
   6,009,233,608 bytes allocated in the heap
     622,682,200 bytes copied during GC
         443,240 bytes maximum residency (505 sample(s))
          48,256 bytes maximum slop
               3 MB total memory in use (0 MB lost due to fragmentation)
                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0     10945 colls,     0 par    0.72s    0.63s     0.0001s    0.0004s
  Gen  1       505 colls,     0 par    0.16s    0.13s     0.0003s    0.0005s
  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    2.00s  (  2.13s elapsed)
  GC      time    0.87s  (  0.76s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time    2.89s  (  2.90s elapsed)
  %GC     time      30.3%  (26.4% elapsed)
  Alloc rate    3,009,412,603 bytes per MUT second
  Productivity  69.7% of total user, 69.4% of total elapsed

1
GHC 버전을 포함 해 주셔서 감사합니다!
dfeuer 2015-04-02

2
@dfeuer 결과는 이제 내 질문에 인라인됩니다.
johnchen902 2015

13
시도 할 수있는 또 다른 옵션 : -fno-state-hack. 그런 다음 실제로 세부 사항을 조사해야합니다.
dfeuer 2015

17
나는 너무 많은 세부 사항을 모르지만 기본적으로 프로그램이 생성하는 특정 함수 (즉, IO또는 ST유형에 숨겨진 함수 )가 한 번만 호출 될 것이라고 추측하는 휴리스틱입니다 . 일반적으로 좋은 추측이지만 잘못된 추측 일 때 GHC는 매우 나쁜 코드를 생성 할 수 있습니다. 개발자들은 꽤 오랫동안 나쁜 것없이 좋은 것을 얻을 수있는 방법을 찾으려고 노력해 왔습니다. 요즘 Joachim Breitner가 작업 중이라고 생각합니다.
dfeuer 2015

2
이것은 ghc.haskell.org/trac/ghc/ticket/10102 와 매우 유사 합니다 . 두 프로그램 모두를 사용 replicateM_하고 GHC는 계산을 외부에서 replicateM_내부 로 잘못 이동 하여 반복합니다.
Joachim Breitner 2015

답변:


42

이 질문이 올바른 답을 얻을 때가 된 것 같습니다.

코드에 무슨 일이 있었는지 -O

주 함수를 확대하고 약간 다시 작성하겠습니다.

main :: IO ()
main = do
    [n, m] <- fmap (map read . words) getLine
    line <- getLine
    let nodes = listArray (0, n) . tonodes n . map (subtract 1) . map read . words $ line
    replicateM_ m $ query n nodes

분명히 여기서 의도는는 NodeArray한 번 생성 된 다음의 모든 m호출에 사용 된다는 것 입니다 query.

불행히도 GHC는이 코드를

main = do
    [n, m] <- fmap (map read . words) getLine
    line <- getLine
    replicateM_ m $ do
        let nodes = listArray (0, n) . tonodes n . map (subtract 1) . map read . words $ line
        query n nodes

여기서 문제를 즉시 확인할 수 있습니다.

상태 해킹이란 무엇이며 왜 내 프로그램 성능을 파괴합니까?

그 이유는 (대략) "어떤 것이 유형일 때 IO a한 번만 호출된다고 가정합니다."라고 말하는 상태 해킹 때문입니다 . 공식 문서는 훨씬 더 정교한되지 않습니다 :

-fno-state-hack

State # 토큰을 인수로 사용하는 모든 람다가 단일 항목으로 간주되는 "상태 해킹"을 끄십시오. 따라서 내부 항목을 인라인해도 괜찮은 것으로 간주됩니다. 이것은 IO 및 ST 모나드 코드의 성능을 향상시킬 수 있지만 공유를 줄일 위험이 있습니다.

대략적인 아이디어는 다음과 같습니다. IO유형과 where 절 을 사용하여 함수를 정의하는 경우 , 예를 들어

foo x = do
    putStrLn y
    putStrLn y
  where y = ...x...

유형의 무언가는 유형의 무언가 IO a로 볼 수 있습니다 RealWord -> (a, RealWorld). 그 관점에서 위는 (대략)

foo x = 
   let y = ...x... in 
   \world1 ->
     let (world2, ()) = putStrLn y world1
     let (world3, ()) = putStrLn y world2
     in  (world3, ())

foowill (일반적으로) 호출 은 다음과 같습니다 foo argument world. 그러나의 정의 foo는 하나의 인수 만 취하고 다른 하나는 나중에 로컬 람다 식에 의해 소비됩니다! 그것은 매우 느린 호출이 될 것 foo입니다. 코드가 다음과 같으면 훨씬 빠를 것입니다.

foo x world1 = 
   let y = ...x... in 
   let (world2, ()) = putStrLn y world1
   let (world3, ()) = putStrLn y world2
   in  (world3, ())

이 ETA-확장을 불러 다양한 이유로 수행 (예입니다 함수의 정의를 분석 하여, 이 호출되고 있는지 확인 하고, -이 경우에는 - 유형 감독 추론을).

불행히도 호출 foo이 실제로 형식 let fooArgument = foo argument, 즉 인수가 있지만 world전달되지 않은 경우 (아직) 성능이 저하됩니다 . 원래 코드에서 fooArgument여러 번 사용 y되면 여전히 한 번만 계산되고 공유됩니다. 수정 된 코드에서는 y매번 다시 계산됩니다. 정확히 nodes.

문제를 해결할 수 있습니까?

혹시. 이를 시도하려면 # 9388 을 참조하십시오 . 문제를 해결하는 데있어 문제 는 컴파일러가 확실히 알 수 없더라도 변환이 정상적으로 발생하는 많은 경우 성능이 저하 된다는 것 입니다 . 기술적으로 괜찮지 않은 경우가있을 수 있습니다. 즉, 공유가 손실되지만 더 빠른 호출로 인한 속도 향상이 재 계산의 추가 비용보다 크기 때문에 여전히 유용합니다. 그래서 여기서 어디로 가야할지 명확하지 않습니다.


4
매우 흥미로운! 그러나 나는 그 이유를 잘 이해하지 못했습니다. "다른 하나는 나중에 로컬 람다 표현식에 의해서만 소비됩니다! foo" "에 대한 매우 느린 호출이 될 것입니다.
IMZ - 이반 Zakharyaschev

특정 로컬 사례에 대한 해결 방법이 있습니까? -f-no-state-hack컴파일이 꽤 무거워 보일 때. {-# NOINLINE #-}당연한 것 같지만 여기서 어떻게 적용할지 생각할 수 없습니다. 아마도 nodesIO 작업을 수행하고 >>=? 의 시퀀스에 의존하는 것으로 충분할 것 입니다 .
Barend Venter

나는 또한 도움으로 대체 replicateM_ n foo하는 것을 보았다 forM_ (\_ -> foo) [1..n].
요아킴 Breitner
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.