Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>About a year ago I was working this area for <a href="http://libcxx.llvm.org/" rel="noreferrer">libc++</a> while implementing the unordered (hash) containers for C++11. I thought I would share my experiences here. This experience supports <a href="https://stackoverflow.com/questions/4475996/given-prime-number-n-compute-the-next-prime/4476015#4476015">marcog's accepted answer</a> for a <em>reasonable</em> definition of "brute force".</p> <blockquote> <p>That means that even a simple brute force will be fast enough in most circumstances, taking O(ln(p)*sqrt(p)) on average.</p> </blockquote> <p>I developed several implementations of <code>size_t next_prime(size_t n)</code> where the spec for this function is:</p> <blockquote> <p><em>Returns:</em> The smallest prime that is greater than or equal to <code>n</code>.</p> </blockquote> <p>Each implementation of <code>next_prime</code> is accompanied by a helper function <code>is_prime</code>. <code>is_prime</code> should be considered a private implementation detail; not meant to be called directly by the client. Each of these implementations was of course tested for correctness, but also tested with the following performance test:</p> <pre><code>int main() { typedef std::chrono::high_resolution_clock Clock; typedef std::chrono::duration&lt;double, std::milli&gt; ms; Clock::time_point t0 = Clock::now(); std::size_t n = 100000000; std::size_t e = 100000; for (std::size_t i = 0; i &lt; e; ++i) n = next_prime(n+1); Clock::time_point t1 = Clock::now(); std::cout &lt;&lt; e/ms(t1-t0).count() &lt;&lt; " primes/millisecond\n"; return n; } </code></pre> <p>I should stress that this is a performance test, and does not reflect typical usage, which would look more like:</p> <pre><code>// Overflow checking not shown for clarity purposes n = next_prime(2*n + 1); </code></pre> <p>All performance tests were compiled with:</p> <pre><code>clang++ -stdlib=libc++ -O3 main.cpp </code></pre> <p><strong>Implementation 1</strong></p> <p>There are seven implementations. The purpose for displaying the first implementation is to demonstrate that if you fail to stop testing the candidate prime <code>x</code> for factors at <code>sqrt(x)</code> then you have failed to even achieve an implementation that could be classified as brute force. This implementation is <strong>brutally slow</strong>.</p> <pre><code>bool is_prime(std::size_t x) { if (x &lt; 2) return false; for (std::size_t i = 2; i &lt; x; ++i) { if (x % i == 0) return false; } return true; } std::size_t next_prime(std::size_t x) { for (; !is_prime(x); ++x) ; return x; } </code></pre> <p>For this implementation only I had to set <code>e</code> to 100 instead of 100000, just to get a reasonable running time:</p> <pre><code>0.0015282 primes/millisecond </code></pre> <p><strong>Implementation 2</strong></p> <p>This implementation is the slowest of the <em>brute force</em> implementations and the only difference from implementation 1 is that it stops testing for primeness when the factor surpasses <code>sqrt(x)</code>.</p> <pre><code>bool is_prime(std::size_t x) { if (x &lt; 2) return false; for (std::size_t i = 2; true; ++i) { std::size_t q = x / i; if (q &lt; i) return true; if (x % i == 0) return false; } return true; } std::size_t next_prime(std::size_t x) { for (; !is_prime(x); ++x) ; return x; } </code></pre> <p>Note that <code>sqrt(x)</code> isn't directly computed, but inferred by <code>q &lt; i</code>. This speeds things up by a factor of thousands:</p> <pre><code>5.98576 primes/millisecond </code></pre> <p>and validates marcog's prediction:</p> <blockquote> <p>... this is well within the constraints of most problems taking on the order of a millisecond on most modern hardware.</p> </blockquote> <p><strong>Implementation 3</strong></p> <p>One can nearly double the speed (at least on the hardware I'm using) by avoiding use of the <code>%</code> operator:</p> <pre><code>bool is_prime(std::size_t x) { if (x &lt; 2) return false; for (std::size_t i = 2; true; ++i) { std::size_t q = x / i; if (q &lt; i) return true; if (x == q * i) return false; } return true; } std::size_t next_prime(std::size_t x) { for (; !is_prime(x); ++x) ; return x; } 11.0512 primes/millisecond </code></pre> <p><strong>Implementation 4</strong></p> <p>So far I haven't even used the common knowledge that 2 is the only even prime. This implementation incorporates that knowledge, nearly doubling the speed again:</p> <pre><code>bool is_prime(std::size_t x) { for (std::size_t i = 3; true; i += 2) { std::size_t q = x / i; if (q &lt; i) return true; if (x == q * i) return false; } return true; } std::size_t next_prime(std::size_t x) { if (x &lt;= 2) return 2; if (!(x &amp; 1)) ++x; for (; !is_prime(x); x += 2) ; return x; } 21.9846 primes/millisecond </code></pre> <p>Implementation 4 is probably what most people have in mind when they think "brute force".</p> <p><strong>Implementation 5</strong></p> <p>Using the following formula you can easily choose all numbers which are divisible by neither 2 nor 3:</p> <pre><code>6 * k + {1, 5} </code></pre> <p>where k >= 1. The following implementation uses this formula, but implemented with a cute xor trick:</p> <pre><code>bool is_prime(std::size_t x) { std::size_t o = 4; for (std::size_t i = 5; true; i += o) { std::size_t q = x / i; if (q &lt; i) return true; if (x == q * i) return false; o ^= 6; } return true; } std::size_t next_prime(std::size_t x) { switch (x) { case 0: case 1: case 2: return 2; case 3: return 3; case 4: case 5: return 5; } std::size_t k = x / 6; std::size_t i = x - 6 * k; std::size_t o = i &lt; 2 ? 1 : 5; x = 6 * k + o; for (i = (3 + o) / 2; !is_prime(x); x += i) i ^= 6; return x; } </code></pre> <p>This effectively means that the algorithm has to check only 1/3 of the integers for primeness instead of 1/2 of the numbers and the performance test shows the expected speed up of nearly 50%:</p> <pre><code>32.6167 primes/millisecond </code></pre> <p><strong>Implementation 6</strong></p> <p>This implementation is a logical extension of implementation 5: It uses the following formula to compute all numbers which are not divisible by 2, 3 and 5:</p> <pre><code>30 * k + {1, 7, 11, 13, 17, 19, 23, 29} </code></pre> <p>It also unrolls the inner loop within is_prime, and creates a list of "small primes" that is useful for dealing with numbers less than 30.</p> <pre><code>static const std::size_t small_primes[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 }; static const std::size_t indices[] = { 1, 7, 11, 13, 17, 19, 23, 29 }; bool is_prime(std::size_t x) { const size_t N = sizeof(small_primes) / sizeof(small_primes[0]); for (std::size_t i = 3; i &lt; N; ++i) { const std::size_t p = small_primes[i]; const std::size_t q = x / p; if (q &lt; p) return true; if (x == q * p) return false; } for (std::size_t i = 31; true;) { std::size_t q = x / i; if (q &lt; i) return true; if (x == q * i) return false; i += 6; q = x / i; if (q &lt; i) return true; if (x == q * i) return false; i += 4; q = x / i; if (q &lt; i) return true; if (x == q * i) return false; i += 2; q = x / i; if (q &lt; i) return true; if (x == q * i) return false; i += 4; q = x / i; if (q &lt; i) return true; if (x == q * i) return false; i += 2; q = x / i; if (q &lt; i) return true; if (x == q * i) return false; i += 4; q = x / i; if (q &lt; i) return true; if (x == q * i) return false; i += 6; q = x / i; if (q &lt; i) return true; if (x == q * i) return false; i += 2; } return true; } std::size_t next_prime(std::size_t n) { const size_t L = 30; const size_t N = sizeof(small_primes) / sizeof(small_primes[0]); // If n is small enough, search in small_primes if (n &lt;= small_primes[N-1]) return *std::lower_bound(small_primes, small_primes + N, n); // Else n &gt; largest small_primes // Start searching list of potential primes: L * k0 + indices[in] const size_t M = sizeof(indices) / sizeof(indices[0]); // Select first potential prime &gt;= n // Known a-priori n &gt;= L size_t k0 = n / L; size_t in = std::lower_bound(indices, indices + M, n - k0 * L) - indices; n = L * k0 + indices[in]; while (!is_prime(n)) { if (++in == M) { ++k0; in = 0; } n = L * k0 + indices[in]; } return n; } </code></pre> <p>This is arguably getting beyond "brute force" and is good for boosting the speed another 27.5% to:</p> <pre><code>41.6026 primes/millisecond </code></pre> <p><strong>Implementation 7</strong></p> <p>It is practical to play the above game for one more iteration, developing a formula for numbers not divisible by 2, 3, 5 and 7:</p> <pre><code>210 * k + {1, 11, ...}, </code></pre> <p>The source code isn't shown here, but is very similar to implementation 6. This is the implementation I chose to actually use for the unordered containers of <a href="https://github.com/llvm-mirror/libcxx/blob/master/src/hash.cpp" rel="noreferrer">libc++</a>, and that source code is open source (found at the link).</p> <p>This final iteration is good for another 14.6% speed boost to:</p> <pre><code>47.685 primes/millisecond </code></pre> <p>Use of this algorithm assures that clients of <a href="http://libcxx.llvm.org/" rel="noreferrer">libc++</a>'s hash tables can choose any prime they decide is most beneficial to their situation, and the performance for this application is quite acceptable.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload