web-dev-qa-db-ja.com

JavaScriptのエラトステネスアルゴリズムのふるいは、多数のために無限に実行されます

私はJavaScriptで エラトステネスのふるい アルゴリズムを書こうとしています。基本的に、私は文字通り以下の手順に従いました。

  1. 2から(n-1)までの連続する整数のリストを作成します
  2. 最初の素数pを2に等しくします
  3. Pから始めて、pの増分でカウントアップし、これらの各数値(pおよびpの倍数)を削除します。
  4. リスト内の次の番号に移動し、2、3、4を繰り返します
  5. 意図せずに削除された素数をリストに追加します

そしてこれは私が思いついたものです:

function eratosthenes(n){
var array = [];
var tmpArray = []; // for containing unintentionally deleted elements like 2,3,5,7,...
var maxPrimeFactor = 0;
var upperLimit = Math.sqrt(n);
var output = [];

// Eratosthenes algorithm to find all primes under n

// Make an array from 2 to (n - 1)
//used as a base array to delete composite number from
for(var i = 2; i < n; i++){
    array.Push(i);
}

// Remove multiples of primes starting from 2, 3, 5,...
for(var i = array[0]; i < upperLimit; i = array[0]){
    removeMultiples: 
    for(var j = i, k = i; j < n; j += i){
        var index = array.indexOf(j);
        if(index === -1)
            continue removeMultiples;
        else
            array.splice(index,1);
    }
    tmpArray.Push(k);
}
array.unshift(tmpArray);
return array;
}

小さい数では機能しますが、100万を超える数では機能しません。 Node.jsを使用してテストしましたが、プロセスは無限に見え、メモリエラーは表示されません。私は解決策を読みました ここ (これもjavascriptで)が、それでも完全に理解することはできません。

質問:100万以上などの十分な数でこれを機能させるにはどうすればよいですか?

18
Bao

線形時間で実行されるArray#indexOfArray#spliceなどの配列操作関数を利用することで、エラトステネスのふるいをはるかに遅くしています。関係する両方の操作に対してO(1)を持つことができる場合。

以下は、従来のプログラミング手法に従ったエラトステネスのふるいです。

var eratosthenes = function(n) {
    // Eratosthenes algorithm to find all primes under n
    var array = [], upperLimit = Math.sqrt(n), output = [];

    // Make an array from 2 to (n - 1)
    for (var i = 0; i < n; i++) {
        array.Push(true);
    }

    // Remove multiples of primes starting from 2, 3, 5,...
    for (var i = 2; i <= upperLimit; i++) {
        if (array[i]) {
            for (var j = i * i; j < n; j += i) {
                array[j] = false;
            }
        }
    }

    // All array[i] set to true are primes
    for (var i = 2; i < n; i++) {
        if(array[i]) {
            output.Push(i);
        }
    }

    return output;
};

ここでn = 1 000 000の実例を見ることができます。

36
Alexander

この質問は、「大きな数」が何であるかという定義の低い側で少しけちであり、それがわずか約百万から始まることを受け入れます。 現在の答え は機能します。ただし、ふるいにかける各要素に1つの8バイト数(64ビットの2倍の実数)、見つかったすべての素数に別の8バイト数のように、かなり多くのメモリを使用します。この答えは、JavaScript実行マシンで使用可能なメモリの量を超えるため、約2億5000万以上の「多数」では機能しません。

Eratosthenesの「無限」(無制限)ページセグメント化ふるいを実装する次のJavaScriptコードは、ビットパックされた16キロバイトのページセグメント化ふるいバッファを1つだけ使用し(1ビットは1つの潜在的な素数を表す)、ストレージのみを使用するという点でこの問題を克服します。現在のページセグメントの現在の最大数の平方根までの基本素数。実際に見つかった素数は、ストレージを必要とせずに順番に列挙されます。また、偶数の素数は2だけなので、奇数のコンポジットのみをふるいにかけることで時間を節約できます。

var SoEPgClass = (function () {
  function SoEPgClass() {
    this.bi = -1; // constructor resets the enumeration to start...
  }
  SoEPgClass.prototype.next = function () {
    if (this.bi < 1) {
      if (this.bi < 0) {
        this.bi++;
        this.lowi = 0; // other initialization done here...
        this.bpa = [];
        return 2;
      } else { // bi must be zero:
        var nxt = 3 + 2 * this.lowi + 262144; //just beyond the current page
        this.buf = [];
        for (var i = 0; i < 2048; i++) this.buf.Push(0); // faster initialization 16 KByte's:
        if (this.lowi <= 0) { // special culling for first page as no base primes yet:
          for (var i = 0, p = 3, sqr = 9; sqr < nxt; i++, p += 2, sqr = p * p)
            if ((this.buf[i >> 5] & (1 << (i & 31))) === 0)
              for (var j = (sqr - 3) >> 1; j < 131072; j += p)
                this.buf[j >> 5] |= 1 << (j & 31);
        } else { // other than the first "zeroth" page:
          if (!this.bpa.length) { // if this is the first page after the zero one:
            this.bps = new SoEPgClass(); // initialize separate base primes stream:
            this.bps.next(); // advance past the only even prime of 2
            this.bpa.Push(this.bps.next()); // keep the next prime (3 in this case)
          }
          // get enough base primes for the page range...
          for (var p = this.bpa[this.bpa.length - 1], sqr = p * p; sqr < nxt;
            p = this.bps.next(), this.bpa.Push(p), sqr = p * p);
          for (var i = 0; i < this.bpa.length; i++) { //for each base prime in the array
            var p = this.bpa[i];
            var s = (p * p - 3) >> 1; //compute the start index of the prime squared
            if (s >= this.lowi) // adjust start index based on page lower limit...
              s -= this.lowi;
            else { //for the case where this isn't the first prime squared instance
              var r = (this.lowi - s) % p;
              s = (r != 0) ? p - r : 0;
            }
            //inner tight composite culling loop for given prime number across page
            for (var j = s; j < 131072; j += p) this.buf[j >> 5] |= 1 << (j & 31);
          }
        }
      }
    }
    //find next marker still with prime status
    while (this.bi < 131072 && this.buf[this.bi >> 5] & (1 << (this.bi & 31))) this.bi++;
    if (this.bi < 131072) // within buffer: output computed prime
      return 3 + ((this.lowi + this.bi++) * 2);
    else { // beyond buffer range: advance buffer
      this.bi = 0;
      this.lowi += 131072;
      return this.next(); // and recursively loop just once to make a new page buffer
    }
  };
  return SoEPgClass;
})();

上記のコードは、次のJavaScriptコードによって、指定された制限まで素数をカウントするために使用できます。

window.onload = function () {
  var elpsd = -new Date().getTime();
  var top_num = 1000000000;
  var cnt = 0;
  var gen = new SoEPgClass();
  while (gen.next() <= top_num) cnt++;
  elpsd += (new Date()).getTime();
  document.getElementById('content')
    .innerText = 'Found ' + cnt + ' primes up to ' + top_num + ' in ' + elpsd + ' milliseconds.';
};

上記の2つのJavaScriptコードを、whatever.htmlという名前の次のHTMLコードと同じフォルダー内のapp.jsという名前のファイルに入れると、ブラウザーでHTMLファイルを開くことでコードを実行できます。

<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Page Segmented Sieve of Eratosthenes in JavaScript</title>
    <script src="app.js"></script>
  </head>
  <body>
    <h1>Page Segmented Sieve of Eratosthenes in JavaScript.</h1>

    <div id="content"></div>
  </body>
</html>

このコードは、Google ChromeのV8エンジンなどのJust-In-Time(JIT)コンパイルを使用してJavaScript実行エンジンで実行すると、10億の範囲にふるいにかけることができます。極端なホイール因数分解と最低ベース素数のページバッファの事前カリングを使用することで、さらなるゲインを実現できます。この場合、実行される作業量をさらに4分の1に削減できます。つまり、素数の数をカウントできます。コードの複雑さが増すという犠牲を払ってではありますが、数秒で最大10億になります(ここで使用されているようにカウントには列挙は必要ありませんが、ページセグメントバッファーでビットカウント手法を直接使用できます)。

EDIT_ADD:

TypedArrayとECMAScript2015(現在すべての一般的なブラウザーでサポートされている)のasm.js最適化を使用し、コードを次のように変更することで、実行速度を3倍以上高速化できます。

"use strict";
var SoEPgClass = (function () {
  function SoEPgClass() {
    this.bi = -1; // constructor resets the enumeration to start...
    this.buf = new Uint8Array(16384);
  }
  SoEPgClass.prototype.next = function () {
    if (this.bi < 1) {
      if (this.bi < 0) {
        this.bi++;
        this.lowi = 0; // other initialization done here...
        this.bpa = [];
        return 2;
      } else { // bi must be zero:
        var nxt = 3 + 2 * this.lowi + 262144; // just beyond the current page
        for (var i = 0; i < 16384; ++i) this.buf[i] = 0 >>> 0; // zero buffer
        if (this.lowi <= 0) { // special culling for first page as no base primes yet:
          for (var i = 0, p = 3, sqr = 9; sqr < nxt; ++i, p += 2, sqr = p * p)
            if ((this.buf[i >> 3] & (1 << (i & 7))) === 0)
              for (var j = (sqr - 3) >> 1; j < 131072; j += p)
                this.buf[j >> 3] |= 1 << (j & 7);
        } else { // other than the first "zeroth" page:
          if (!this.bpa.length) { // if this is the first page after the zero one:
            this.bps = new SoEPgClass(); // initialize separate base primes stream:
            this.bps.next(); // advance past the only even prime of 2
            this.bpa.Push(this.bps.next()); // keep the next prime (3 in this case)
          }
          // get enough base primes for the page range...
          for (var p = this.bpa[this.bpa.length - 1], sqr = p * p; sqr < nxt;
            p = this.bps.next(), this.bpa.Push(p), sqr = p * p);
          for (var i = 0; i < this.bpa.length; ++i) { // for each base prime in the array
            var p = this.bpa[i] >>> 0;
            var s = (p * p - 3) >>> 1; // compute the start index of the prime squared
            if (s >= this.lowi) // adjust start index based on page lower limit...
              s -= this.lowi;
            else { // for the case where this isn't the first prime squared instance
              var r = (this.lowi - s) % p;
              s = (r != 0) ? p - r : 0;
            }
            if (p <= 8192) {
              var slmt = Math.min(131072, s + (p << 3));
              for (; s < slmt; s += p) {
                var msk = (1 >>> 0) << (s & 7);
                for (var j = s >>> 3; j < 16384; j += p) this.buf[j] |= msk;
              }
            }
            else
              // inner tight composite culling loop for given prime number across page
              for (var j = s; j < 131072; j += p) this.buf[j >> 3] |= (1 >>> 0) << (j & 7);
          }
        }
      }
    }
    //find next marker still with prime status
    while (this.bi < 131072 && this.buf[this.bi >> 3] & ((1 >>> 0) << (this.bi & 7)))
      this.bi++;
    if (this.bi < 131072) // within buffer: output computed prime
      return 3 + ((this.lowi + this.bi++) << 1);
    else { // beyond buffer range: advance buffer
      this.bi = 0;
      this.lowi += 131072;
      return this.next(); // and recursively loop just once to make a new page buffer
    }
  };
  return SoEPgClass;
})();

事前に型指定されたECMAScriptプリミティブ配列を使用して、配列内の整数を直接使用することでオーバーヘッドを回避し(float表現を使用してスペースを浪費することも回避)、asm.jsを使用して使用可能な型ヒントを使用してビット操作を行うため、スピードアップが機能します。符号なし整数/バイトを使用します。また、配列の割り当てにかかる時間を節約するために、ふるい配列を1回割り当て、新しいページセグメントごとにゼロにするようになりました。現在、ローエンドの1.92ギガヘルツCPUでは約50秒ではなく、約16秒で10億にふるいにかけられます。また、アルゴリズムが変更されて、内部の合成数表現(ビットパックビット単位)が簡略化され、カリング操作の大部分である小さな素数の速度が向上します。

現在、費やされた時間の約60%が、見つかった素数を列挙するためだけに費やされていることに注意してください。これは、各セグメントページの配列内のゼロビットの数を合計するだけで、見つかった素数をカウントするようなふるいの通常の使用では大幅に減らすことができます。これが行われた場合、10億にふるいにかける時間は、このローエンドCPUで7秒に近いものになり、さらにいくつかの最適化が可能になる可能性があります(Google Chromeバージョン72を使用したすべてのタイミング絶えず改善されており、それ以降のバージョンはより高速に実行される可能性があるV8 JavaScriptエンジン)。

TBH、私は個人的にJavaScriptを「現代」言語にするために必要なすべての拡張機能と複雑さを嫌い、特に動的型付けは好きではないので、数年前に登場したMicrosoftのTypeScriptを採用しました。上記のコードは、実際には、静的に型付けされたオブジェクト指向プログラミング(OOP)に重点を置いた、TypeScriptからの出力としてのコードの変更です。 「プロトタイプ」にメソッドを追加する標準的な方法による「次の」インスタンスメソッドの呼び出しは、関数を呼び出すよりもはるかに遅い可能性があることに気付いたので、それをテストしたところ、次のようになりました。 この実行可能なリンク 列挙を単純な出力クロージャ関数に変更するだけで、見つかった素数を約2.5倍速く列挙します。

これで、 コードが変更されたこの他の実行可能なリンク に示すように、見つかった素数の数を数えるだけで、素数の列挙を完全に排除できます。これは、上記の改善があっても、見つかった素数の列挙にはまだコストがかかることを示しています。実行可能なコードへの上記の2つのリンクの実行時間の差として列挙時間を決定できるものを使用して、このアルゴリズムで実際にふるい分けを行うのとほぼ同じ時間。

リンクの実行時間は、ここで説明したものとは異なる(そしておそらく短い)ことに注意してください。現在使用しているほとんどのCPUは、現在使用しているタブレットWindows CPU(1.92ギガヘルツのIntel x5-Z3850)よりも高速で強力です。 JavaScriptは、リンクを表示しているマシンで実行されます。

これにより、JavaScriptはJVMまたはDotNetに実装された同じアルゴリズムよりも少しだけ遅くなります。もちろん、C/C++、Rust、Nim、Haskell、Swiftなどの言語からコンパイルされた高度に最適化されたネイティブコードよりもはるかに遅くなります。このローエンドCPUで約2秒でこのアルゴリズムを実行できるFreePascal、Juliaなど。 WebAssemblyは、ブラウザーの実装にもよりますが、このアルゴリズムをここのJavaScriptよりも最大で約2〜3倍高速に実行できます。また、WebAssembly仕様が完全に完成して実装されると、使用される有効なコアの数に応じてさらにゲインを上げるためのマルチスレッドサポートが提供されます。

END_EDIT_ADD

EDIT_ADD_MORE:

見つかった素数を列挙するのではなく効率的にカウントするために上記のかなり小さな変更が行われると、それらをふるいにかけるよりもカウント時間がわずかなオーバーヘッドになります。最大のホイール因数分解を使用するために、より広範な変更を行う価値があります( 「奇数のみ」の場合は2だけでなく、210の潜在的な素数のスパンをカバーするホイールの場合は3、5、および7)、小さなふるいアレイの初期化を事前にカリングすることにより、 11、13、17、19の次の素数でカリングします。これにより、ページセグメント化ふるいを使用する場合の複合数カリング操作の数が約4分の1から10億の範囲に削減され、実行されるように記述できます。上記のコードの場合とほぼ同じ速度で各カリング操作の操作が減少するため、約4倍の速度になります。

210スパンのホイール因数分解を効率的に行う方法は、「奇数のみ」のふるい分けを効率的に行うこの方法に従うことです。上記の現在のアルゴリズムは、他の平面が2つのうち1つのビットパック平面をふるいにかけると考えることができます。 2を超える偶数のみが含まれているため、削除できます。 210スパンの場合、11以上の可能な素数を表すこのサイズの48ビットパック配列を定義できます。他のすべての162平面には、2、3、5、または7の因数である数が含まれているため、次のことを行う必要はありません。考慮されます。このように、少ないメモリ要件でふるい分けするのと同じくらい効率的です(「奇数のみ」と比較して半分以上、1つの48プレーンの「ページ」が16キロバイト= 131072ビット/プレーンを表すここと同じくらいの効率)ふるいページセグメントあたり27,525,120の数の範囲である210倍、したがって、(上記のほぼ4000ではなく)10億までふるいにかけるのは40ページセグメントのみであり、したがって、ページセグメントあたりのベースプライムあたりの開始アドレス計算のオーバーヘッドが少なくなります。効率がさらに向上します。

上記の拡張コードは数百行で、ここに投稿するには長いですが、Google V8JavaScriptエンジンを使用するローエンドのIntel1.92 Gigahertz CPUでは、2秒未満で素数を10億に数えることができます。これは約4です。ネイティブコードで実行される同じアルゴリズムよりも5倍遅くなります。これは、JavaScriptで実行できることの限界についてであり、「ループ展開」および(もちろん)マルチプロセッシングのさらに高度な手法は利用できません。ただし、約1.4秒で実行されるこのローエンドCPUでのSieve ofAtkinの手動最適化リファレンスC実装とほぼ一致するだけで十分です。

上記のコードは約160億の範囲まで非常に効率的ですが、他の改善により、数万から数万のさらに広い範囲まで効率を維持できるため、数個で約1e14までの素数の数を数えることができます。より高速なCPUでJavaScriptを使用する日数。この範囲の素数の数は1985年まで知られていなかったため、これは興味深いことです。当時のコンピューターは、エラトステネスのふるいをその範囲で十分な速度で実行できるほど強力ではなかったため、数値解析手法によって決定されました。妥当な時間。

私の現在の「反JavaScript」とプロ関数型コーディングスタイルのバイアスで、JavaScriptに非常に変換されるF#(必要に応じてOOPもサポートする静的に型付けされたML「関数型」言語)の実装であるFableを使用してこのコードを記述します生成されたコードがJavaScriptで直接記述されている場合とほぼ同じくらい高速になるように効率的に。

コードがChrome V8 JavaScriptエンジンでFable(Elmish Reactインターフェースを使用)を使用)で、前回と同じように純粋なJavaScriptを作成するのとほぼ同じ速度で実行できることを示すため上記のリンク これはオンラインのFableへのリンクですIDE上記のアルゴリズムを含む 。純粋なJavaScriptよりも実行速度が少し遅く、JavaScript出力の「コード」ビューに次のように表示されます。理由:Tail Call Optimization(TCO)用に生成されたコードは、JavaScriptの場合のように単純なループではありません。同じ速度を得るために、タイトな内部カリングループのためだけにコードを手動で調整するのは簡単です。コードは書かれています。配列コンテンツの変更を除いて機能的なスタイルで、必要に応じて、理解しやすいようにJavaScriptと同じ形式のシーケンスジェネレーター関数を使用します。コードのこのストリーミングジェネレーター部分がF#を使用するように記述されている場合は、ほぼ同じ速度で動作します。目に見える変異のない配列。

上記のFableコードは純粋なF#であるため、FabulousライブラリをDotNet CoreのJavaScriptジェネレーターとして実行することも、DotNetCoreで直接実行することでマルチプラットフォームで少し高速に実行することもできます。

END_EDIT_ADD_MORE

要約すると、1秒のオーダーで数百万までの素数を見つけることができるあらゆる種類のアルゴリズムがありますが、実行時間のオーダーで数十億までの素数を決定するには、効率的なページセグメント化配列ベースのエラトステネスのふるいアルゴリズムが必要です。

10
GordonBGood

これをアレクサンダーへのコメントとして投稿しますが、そうするという評判はありません。彼の答えは素晴らしいです、そしてこれはそれをより速くするためにそれを微調整するだけです。 n = 100,000,000をテストしてベンチマークを行いました。

'array'でtrueとfalseを使用する代わりに、1と0を使用することで、速度が大幅に向上します。これにより、Chromeが5000ミリ秒から4250ミリ秒に短縮されました。Firefoxは影響を受けませんでした(5600いずれかの方法でms)。

そうすれば、偶数が素数になることは決してないということを考慮に入れることができます。 2を「出力」に入れると、i = 3を実行できます。ふるい中のi + = 2、およびj + = i * 2(偶数の倍数は偶数であるため、偶数の倍数をスキップできます)、i + = 2で、終わり。これにより、Chromeの時間が4250ミリ秒から3350ミリ秒に短縮されました。Firefoxのメリットは少し少なくなり、5600ミリ秒から4800ミリ秒に短縮されました。

とにかく、これら2つの調整を組み合わせることで、Chromeで33%の速度向上、Firefoxで14%の速度向上が実現しました。これがAlexanderのコードの改良版です。

var eratosthenes = function(n) {
    // Eratosthenes algorithm to find all primes under n
    var array = [], upperLimit = Math.sqrt(n), output = [2];

    // Make an array from 2 to (n - 1)
    for (var i = 0; i < n; i++)
        array.Push(1);

    // Remove multiples of primes starting from 2, 3, 5,...
    for (var i = 3; i <= upperLimit; i += 2) {
        if (array[i]) {
            for (var j = i * i; j < n; j += i*2)
                array[j] = 0;
        }
    }

    // All array[i] set to 1 (true) are primes
    for (var i = 3; i < n; i += 2) {
        if(array[i]) {
            output.Push(i);
        }
    }

    return output;
};
5
deathwombat

楽しみのために、TDDのルールに厳密に従ってErastoten sieveアルゴリズム(Nodeで実行)を実装しました。このバージョンは、学校の演習として、または私がそうであったように、インタビューには十分なはずです-少しいじりまわします。

受け入れられた答えはGordonBGoodによって提供されたものでなければならないと私は間違いなく思います。

module.exports.compute = function( size )
{
    if ( !utils.isPositiveInteger( size ) )
    {
        throw new TypeError( "Input must be a positive integer" );
    }

    console.time('optimal');
    console.log();
    console.log( "Starting for optimal computation where size = " + size );
    let sieve = utils.generateArraySeq( 2, size );

    let prime = 2;
    while ( prime )
    {
        // mark multiples
        for ( let i = 0; i < sieve.length; i += prime )
        {
            if ( sieve[i] !== prime )
            {
                sieve[i] = -1;
            }
        }

        let old_prime = prime;
        // find next prime number
        for ( let i = 0; i < sieve.length; i++ )
        {
            if ( ( sieve[i] !== -1 ) && ( sieve[i] > prime ) )
            {
                prime = sieve[i];
                break;
            }
        }

        if ( old_prime === prime )
        {
            break;
        }
    }
    console.timeEnd('optimal');
    // remove marked elements from the array
    return sieve.filter( 
        function( element )
        {
            return element !== -1;
        } );
} // compute

意味のある批評をいただければ幸いです。

リポジトリ全体は私のgithubアカウントにあります。

2
Igor L.

私はパーティーに少し遅れているので。 100までのすべての素数を見つける私の単純で少しハッキーな貢献を追加したいと思います:

<!DOCTYPE html>
<html>
<title>Primes</title>
<head>
<script>
function findPrimes() {
    var primes = []
    var search = []

    var maxNumber = 100
    for(var i=2; i<maxNumber; i++){
        if(search[i]==undefined){
            primes.Push(i);
            for(var j=i+i; j<maxNumber; j+=i){
                search[j] = 0;
            }
        }
    }
   document.write(primes);
}
findPrimes();
</script>
</head>
<body>
</body>
</html>
0
Kent Kostelac
function sieveOfEratosthenes(num, fromSt = null) {
let boolArr = Array(num + 1).fill(true); // Taking num+1 for simplicity
boolArr[0] = false;
boolArr[1] = false;

for (
    let divisor = 2;
    divisor * divisor <= num;
    divisor = boolArr.indexOf(true, divisor + 1)
)
    for (let j = 2 * divisor; j <= num; j += divisor) boolArr[j] = false;

let primeArr = [];
for (
    let idx = fromSt || boolArr.indexOf(true);
    idx !== -1;
    idx = boolArr.indexOf(true, idx + 1)
)
    primeArr.Push(idx);

return primeArr;
}
0
Abhay Kumar