- 100までの整数から素数を列挙せよ…を Squeak の Smalltalk で
Smalltalk のオーソドックスな方法で。 - 100までの整数から素数を列挙せよ…を Smalltalk-76 で
Smalltalk の祖先で。 - 100までの整数から素数を列挙せよ…を Smalltalk-72 で
Smalltalk の祖先の祖先で。 - 100までの整数から素数を列挙せよ…を bitblt で
ビット演算ルーチンで。 - 100までの整数から素数を列挙せよ…を Squeak の eToys で
タートルグラフィックスで。
素数を列挙するにあたって、エラトステネスのふるいは比較的高速で有効です。たとえば、404 Blog Not Found:LLR2006 - 1,000,000(番目|まで)の素数 で、いくつかの言語で例示されているのとほぼ同じことをする Squeak を使った Smalltalk の次のコード…
[ | max primes isPrime | max := 1000000. primes := ReadWriteStream on: #(). isPrime := [:n | | flag d | flag := true. primes reset. [flag not or: [primes atEnd or: [(d := primes next) * d > n]]] whileFalse: [n \\ d = 0 ifTrue: [flag := false]]. flag ifTrue: [primes setToEnd; nextPut: n] ifFalse: [0]]. (2 to: max) do: [:i | isPrime value: i]. primes contents size ] timeToRun / 1.0e3
を用い、1000000 までの素数が 78498 個であることを知ろうとしたとき(ブロック内を選択して print it (alt/cmd + p) で値が、全体の選択で算出に要した時間が得られます…)、手元の PowerBook G4 Ti 1GHz(Mac OS X 10.4.6)では 12 秒ほどかかりますが、エラトステネスのふるいを使ったメソッド「Integer class >> #primesUpTo:」ならば、同じ Smalltalk で書かれているにもかかわらず1秒とかかりません。
[(Integer primesUpTo: 1000000) size] timeToRun / 1.0e3 " => 0.884 "
比較のために、手元の同じ環境で さんの Perl と Ruby のコードを走らせたときは、こんな結果が出ています。私の古〜い PowerBook は、小飼さんの最新式の環境(MacBook Pro)の3〜4割程度のパワーしかないこともわかります。w orz
$ perl -v This is perl, v5.8.7 built for darwin-2level ... $ /usr/bin/time perl primesupto.pl 1000000 27.77 real 24.83 user 0.14 sys
$ ruby -v ruby 1.8.4 (2005-12-24) [powerpc-darwin8.5.0] $ /usr/bin/time ruby primesupto.rb 1000000 85.78 real 77.39 user 0.37 sys
ところで、この #primesUpTo: にはエラトステネスのふるいの「メモリを喰う」という弱点にも対策が施されていて、キミならどう書く 2.0 - ROUND 1 - 00:26 - mputの日記。 (2006-06-17) で提案されているお題である、1000000 個目の素数である 15485863 にも、15 秒強で、メモリを食い尽くすことなく到達できます。
(Integer primesUpTo: 15485863 + 1) size " => 1000000 "
[(Integer primesUpTo: 15485863 + 1) size] timeToRun / 1.0e3 " => 15.5 "
これには John Moyer が公開している方法…
が使われており、Integer class >> #largePrimesUpTo:do: にあるコメントによると、計算上、1073741823(16r40000000 - 1。SmallInteger maxVal)までの素数を得るための“ふるい”には、普通、4ギガバイトほどの容量を必要とするが、この方法で圧縮すると 27 メガバイトほどで足りるのだそうです。すげー。
しくみはちょっとややこしいです。簡単にいうと、ふるいをかける前に、周期的に現われる「確実に素数でない数」を落としておくことで、ふるいに“圧縮”をかけるということだそうです。くだんのページに紹介されている 30 個の整数の周期の場合、30 がすでに 2 * 3 * 5 なので、31 から 60 までの数のうち、素数の可能性のある整数は、30 に 1、7、11、13、17、19、23、29 を足してできる8つの整数に限ることができます。このことは、以降の n 番目の周期(30 * n 。n = 2, 3, 4 ...)についても同じです。
換言すれば、8ビット(8個のフラグ)、つまり1バイトあれば、30個分の整数の素数判定状況を保持することができるわけです。すげー。
なお、くだんのページで公開されている C のプログラムと、Integer class >> #largePrimesUpTo:do: では、64 ビットで 2310(つまり、2 * 3 * 5 * 7 * 11)個の整数の素数判定状況を保存するバージョンが使用されています。
頭の体操を兼ねて、Ruby で同じものを書いてみました。…いや(^_^;)。書いたっちゅうよりは、Integer class >> #largePrimesUpTo:do: を“変換”しただけですので、ほぼ直訳です。本体は小飼さんのサンプルから拝借いたしました。
#!/usr/bin/ruby def primes(upto, &block) return large_primes(upto, &block) if upto > 25000 upto -= 1 flags = Array.new(upto, true) (0...upto-1).each do |i| if flags[i] then prime = i + 2 k = i + prime while k < upto do flags[k] = false k += prime end block.call(prime) end end end def large_primes(upto, &block) idx_limit = Math.sqrt(upto) + 1 flags = Array.new((upto + 2309) / 2310 * 60 + 60, 0xFF) # flags = "\377" * ((upto + 2309) / 2310 * 60 + 60) primes_up_to_2310 = [] primes(2310) {|prime| primes_up_to_2310 << prime} mask_bit_idx = Array.new(2310) bit_idx = -1 mask_bit_idx[0] = (bit_idx += 1) mask_bit_idx[1] = (bit_idx += 1) (0..4).each {|i| block.call(primes_up_to_2310[i])} idx = 5 (2..2309).each do |n| while primes_up_to_2310[idx] < n do idx += 1 end if n == primes_up_to_2310[idx] then mask_bit_idx[n] = (bit_idx += 1) elsif n % 2 == 0 || n % 3 == 0 || n % 5 == 0 || n % 7 == 0 || n % 11 == 0 then mask_bit_idx[n] = 0 else mask_bit_idx[n] = (bit_idx += 1) end end (13...upto).step(2) do |n| if (mask_bit = mask_bit_idx[n % 2310]) != 0 then byte_idx = n / 2310 * 60 + (mask_bit-1 << -3) bit_idx = 1 << (mask_bit & 7) if flags[byte_idx] & bit_idx != 0 then block.call(n) if n < idx_limit then idx = n * n if (idx & 1) == 0 then idx += n end while idx < upto do if (mask_bit = mask_bit_idx[idx % 2310]) != 0 then byte_idx = idx / 2310 * 60 + (mask_bit-1 << -3) mask_bit = 255 - (1 << (mask_bit & 7)) flags[byte_idx] = flags[byte_idx] & mask_bit end idx += 2 * n end end end end end end max = ARGV.size == 1 ? ARGV[0].to_i : 100 primes = [] primes(max) {|prime| primes << prime} puts primes.join(" ")
試してみると、速度的には、前述の余りがゼロかを確認するものに比べて、7〜8倍くらい高速のようです。
$ /usr/bin/time ruby primesupto.ar.rb 1000000 14.07 real 10.76 user 0.10 sys
それでもさすがに Ruby では遅すぎて、私の我慢できる時間では 1000000 個目には到達できませんでした。しかし、top や /proc/self/status でメモリ使用状況を見てみると、large_primes に分岐しないように primes の一行目をコメントアウトした場合と、large_primes を使ったときとでは、断然、large_primes を使ったときの方がメモリの消費は少ないように感じます(UNIX は詳しくないので感じるだけw)。
追記:
我慢して待ってみました(^_^;)。3分半くらいしょうか。
$ /usr/bin/time ruby primesupto.ar.rb 15485863 244.98 real 214.72 user 1.46 sys
ちなみに、nurse さん改良版には負けました(入出力部分は勝手に追加しました)。でも、いい勝負をしていて、遅くてぜんぜんダメってわけでもなさそうです。
$ /usr/bin/time ruby primesupto.nr.rb 15485863 221.45 real 195.28 user 1.12 sys
追記:
コメント欄で はらさんに ruby-list で紹介された方法を教えていただきました。ありがとうございます。これは確かに速いですね。ぶっちぎりです。
$ /usr/bin/time ruby primesupto.ao.rb 15485863 80.99 real 69.66 user 0.43 sys
関連:
- はてなるせだいあり - エラトステネスの篩
nurse さんが、あくなき高速化にチャレンジしておられます。アンドレアス版との比較と改良wも行なってくださいました。 - メモ: 上限を決めずに素数を生成の高速化(Ruby版)
log.ttsky.net さんで、やはり比較をしてくださっています。ありがとうございます。