19

ネタバレ注意:これはプロジェクトオイラーの問題14に関連しています。

次のコードの実行には約15秒かかります。1秒で実行される非再帰的なJavaソリューションがあります。このコードをもっとそれに近づけることができるはずだと思います。

import Data.List

collatz a 1  = a
collatz a x
  | even x    = collatz (a + 1) (x `div` 2)
  | otherwise = collatz (a + 1) (3 * x + 1)

main = do
  print ((foldl1' max) . map (collatz 1) $ [1..1000000])

私はプロファイルを作成し+RHS -p、割り当てられたメモリが大きく、入力が大きくなるにつれて大きくなることに気づきました。n = 100,0001GBには割り当てられます(!)、13GBには割り当てn = 1,000,000られます(!!)。

繰り返しになり-sstderrますが、多くのバイトが割り当てられたものの、合計メモリ使用量は1 MBで、生産性は95%以上だったので、おそらく13GBは赤字です。

私はいくつかの可能性を考えることができます:

  1. 何かが必要なほど厳密ではありません。私はすでに発見 しましfoldl1'たが、多分私はもっとする必要がありますか?厳密としてマークすることは可能collatz ですか(それも意味がありますか?

  2. collatz末尾呼び出しの最適化ではありません。あるべきだと思いますが、確認する方法がわかりません。

  3. コンパイラーはいくつかの最適化を行っていないはずです。たとえば、一度に2つの結果collatz(最大および現在)のみをメモリーに入れる必要があります。

助言がありますか?

これは、なぜこのHaskell式がとても遅いのかとほぼ同じです。ただし、高速Javaソリューションではメモ化を実行する必要がないことに注意してください。それに頼ることなくこれをスピードアップする方法はありますか?

参考までに、私のプロファイリング出力は次のとおりです。

  Wed Dec 28 09:33 2011 Time and Allocation Profiling Report  (Final)

     scratch +RTS -p -hc -RTS

  total time  =        5.12 secs   (256 ticks @ 20 ms)
  total alloc = 13,229,705,716 bytes  (excludes profiling overheads)

COST CENTRE                    MODULE               %time %alloc

collatz                        Main                  99.6   99.4


                                                                                               individual    inherited
COST CENTRE              MODULE                                               no.    entries  %time %alloc   %time %alloc

MAIN                     MAIN                                                   1           0   0.0    0.0   100.0  100.0
 CAF                     Main                                                 208          10   0.0    0.0   100.0  100.0
  collatz                Main                                                 215           1   0.0    0.0     0.0    0.0
  main                   Main                                                 214           1   0.4    0.6   100.0  100.0
   collatz               Main                                                 216           0  99.6   99.4    99.6   99.4
 CAF                     GHC.IO.Handle.FD                                     145           2   0.0    0.0     0.0    0.0
 CAF                     System.Posix.Internals                               144           1   0.0    0.0     0.0    0.0
 CAF                     GHC.Conc                                             128           1   0.0    0.0     0.0    0.0
 CAF                     GHC.IO.Handle.Internals                              119           1   0.0    0.0     0.0    0.0
 CAF                     GHC.IO.Encoding.Iconv                                113           5   0.0    0.0     0.0    0.0

そして-sstderr:

./scratch +RTS -sstderr 
525
  21,085,474,908 bytes allocated in the heap
      87,799,504 bytes copied during GC
           9,420 bytes maximum residency (1 sample(s))          
          12,824 bytes maximum slop               
               1 MB total memory in use (0 MB lost due to fragmentation)  

  Generation 0: 40219 collections,     0 parallel,  0.40s,  0.51s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time   35.38s  ( 36.37s elapsed)
  GC    time    0.40s  (  0.51s elapsed)
  RP    time    0.00s  (  0.00s elapsed)  PROF  time    0.00s  (  0.00s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time   35.79s  ( 36.88s elapsed)  %GC time       1.1%  (1.4% elapsed)  Alloc rate    595,897,095 bytes per MUT second

  Productivity  98.9% of total user, 95.9% of total elapsed

そして、Javaソリューション(私のものではなく、メモ化が削除されたProject Eulerフォーラムから取得):

public class Collatz {
  public int getChainLength( int n )
  {
    long num = n;
    int count = 1;
    while( num > 1 )
    {
      num = ( num%2 == 0 ) ? num >> 1 : 3*num+1;
      count++;
    }
    return count;
  }

  public static void main(String[] args) {
    Collatz obj = new Collatz();
    long tic = System.currentTimeMillis();
    int max = 0, len = 0, index = 0;
    for( int i = 3; i < 1000000; i++ )
    {
      len = obj.getChainLength(i);
      if( len > max )
      {
        max = len;
        index = i;
      }
    }
    long toc = System.currentTimeMillis();
    System.out.println(toc-tic);
    System.out.println( "Index: " + index + ", length = " + max );
  }
}
4

3 に答える 3

21

最初は、 inの前に感嘆符を付けてみるべきだと思いましたcollatz

collatz !a 1  = a
collatz !a x
  | even x    = collatz (a + 1) (x `div` 2)
  | otherwise = collatz (a + 1) (3 * x + 1)

{-# LANGUAGE BangPatterns #-}(これを機能させるには、ソースファイルの先頭に配置する必要があります。)

私の推論は次のようになりました:問題は、collat​​zの最初の引数で大規模なサンクを構築していることです:それは、から始まり1、次になり1 + 1、次に、になり(1 + 1) + 1ます...すべて強制されることはありません。このバングパターンは、呼び出しが行われるたびにの最初の引数をcollatz強制するため、1から始まり、2になり、以下同様に、大きな未評価のサンクを構築することなく、整数のままになります。

bangパターンは、seq;を使用するための省略形であることに注意してください。この場合、collatz次のように書き直すことができます。

collatz a _ | seq a False = undefined
collatz a 1  = a
collatz a x
  | even x    = collatz (a + 1) (x `div` 2)
  | otherwise = collatz (a + 1) (3 * x + 1)

ここでの秘訣は、ガードにaを強制することです。これにより、常にFalseと評価されます(したがって、ボディは無関係です)。その後、評価は次のケースに進み、すでに評価されています。ただし、強打パターンはより明確です。

残念ながら、を使用してコンパイルすると-O2、これは元のファイルよりも高速に実行されません。他に何を試すことができますか?さて、私たちができることの1つは、2つの数値がマシンサイズの整数をオーバーフローしないと想定し、collatzこの型の注釈を付けることです。

collatz :: Int -> Int -> Int

パフォーマンスの問題の根本ではない場合でも、サンクの蓄積を回避する必要があるため、バングパターンはそのままにしておきます。これにより、私の(遅い)コンピューターの時間が8.5秒に短縮されます。

次のステップは、これをJavaソリューションに近づけることです。最初に気付くのは、Haskellでは、div負の整数に関してより数学的に正しい方法で動作しますが、Haskellではと呼ばれる「通常の」C除算よりも遅いということquotです。に置き換えるdivquot、ランタイムが5.2秒に短縮され、Javaソリューションに一致するように(Data.Bitsをインポートする)に置き換えると、4.9秒に短縮されましたx `quot` 2x `shiftR` 1

これは今のところ私が得ることができるのと同じくらい低いですが、これはかなり良い結果だと思います。お使いのコンピュータは私のコンピュータよりも高速なので、Javaソリューションにさらに近いはずです。

これが最終的なコードです(途中で少しクリーンアップしました):

{-# LANGUAGE BangPatterns #-}

import Data.Bits
import Data.List

collatz :: Int -> Int
collatz = collatz' 1
  where collatz' :: Int -> Int -> Int
        collatz' !a 1 = a
        collatz' !a x
          | even x    = collatz' (a + 1) (x `shiftR` 1)
          | otherwise = collatz' (a + 1) (3 * x + 1)

main :: IO ()
main = print . foldl1' max . map collatz $ [1..1000000]

このプログラムのGHCコア(を使用ghc-core)を見ると、これはおそらくほぼ同じくらい良いと思います。ループはボックス化されてcollatzいない整数を使用し、プログラムの残りの部分は問題ないように見えます。私が考えることができる唯一の改善は、map collatz [1..1000000]反復からボクシングを排除することです。

ちなみに、「合計割り当て」の数値については気にしないでください。これは、プログラムの存続期間中に割り当てられた合計メモリであり、GCがそのメモリを再利用しても減少することはありません。複数テラバイトの数字が一般的です。

于 2011-12-28T17:47:46.850 に答える
2

リストと強打パターンを失い、代わりにスタックを使用することで同じパフォーマンスを得ることができます。

import Data.List
import Data.Bits

coll :: Int -> Int
coll 0 = 0
coll 1 = 1
coll 2 = 2
coll n =
  let a = coll (n - 1)
      collatz a 1 = a
      collatz a x
        | even x    = collatz (a + 1) (x `shiftR` 1)
        | otherwise = collatz (a + 1) (3 * x + 1)
  in max a (collatz 1 n)


main = do
  print $ coll 100000

これに関する問題の1つは、1_000_000などの大きな入力の場合はスタックのサイズを大きくする必要があることです。

アップデート:

これは、スタックオーバーフローの問題に悩まされていない末尾再帰バージョンです。

import Data.Word
collatz :: Word -> Word -> (Word, Word)
collatz a x
  | x == 1    = (a,x)
  | even x    = collatz (a + 1) (x `quot` 2)
  | otherwise = collatz (a + 1) (3 * x + 1)

coll :: Word -> Word
coll n = collTail 0 n
  where
    collTail m 1 = m
    collTail m n = collTail (max (fst $ collatz 1 n) m) (n-1)

Wordの代わりにの使用に注意してくださいInt。パフォーマンスに違いがあります。必要に応じて、バングパターンを使用することもできます。これにより、パフォーマンスがほぼ2倍になります。

于 2011-12-29T00:03:48.677 に答える
0

私が見つけた1つのことは、この問題に驚くべき違いをもたらしました。私は折りたたむのではなく、まっすぐな漸化式に固執しました。表現を許してください。書き換え

collatz n = if even n then n `div` 2 else 3 * n + 1

なので

collatz n = case n `divMod` 2 of
            (n', 0) -> n'
            _       -> 3 * n + 1

2.8 GHz Athlon II X4 430 CPUを搭載したシステムで、プログラムの実行時間が1.2秒短縮されました。私の最初のより速いバージョン(divModの使用後2.3秒):

{-# LANGUAGE BangPatterns #-}

import Data.List
import Data.Ord

collatzChainLen :: Int -> Int
collatzChainLen n = collatzChainLen' n 1
    where collatzChainLen' n !l
            | n == 1    = l
            | otherwise = collatzChainLen' (collatz n) (l + 1)

collatz:: Int -> Int
collatz n = case n `divMod` 2 of
                 (n', 0) -> n'
                 _       -> 3 * n + 1

pairMap :: (a -> b) -> [a] -> [(a, b)]
pairMap f xs = [(x, f x) | x <- xs]

main :: IO ()
main = print $ fst (maximumBy (comparing snd) (pairMap collatzChainLen [1..999999]))

おそらくもっと慣用的なHaskellバージョンは約9.7秒で実行されます(divModでは8.5)。それは同じです

collatzChainLen :: Int -> Int
collatzChainLen n = 1 + (length . takeWhile (/= 1) . (iterate collatz)) n

Data.List.Streamを使用すると、このバージョンを明示的な累積と同じように実行するストリームフュージョンが可能になるはずですが、Data.List.Streamを含むUbuntu libghc *パッケージが見つからないため、見つかりません。まだそれを確認してください。

于 2013-11-22T04:53:41.133 に答える