New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
next_prime for lib/prime.rb #1272
Conversation
@@ -132,7 +209,8 @@ def method_added(method) # :nodoc: | |||
# Upper bound of prime numbers. The iterator stops after it | |||
# yields all prime numbers p <= +ubound+. | |||
# | |||
def each(ubound = nil, generator = EratosthenesGenerator.new, &block) | |||
#def each(ubound = nil, generator = EratosthenesGenerator.new, &block) | |||
def each(ubound = nil, generator = NextPrimeGenerator.new, &block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, interesting.
could I have benchmark for this part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following changes makes the code shorter, more readable, and at lease twice as fast (for very large numbers), and is adapted from my primes-utils
gem. See PRIMES-UTILS HANDBOOK for math and code details.
https://www.scribd.com/doc/266461408/Primes-Utils-Handbook
require 'openssl'
class Integer
# Returns true if +self+ passes Miller-Rabin Test on +b+
def miller_rabin_test(b)
return self >= 2 if self <= 3
return false unless [1,5].include?(self % 6)
n = d = self - 1
d >>= 1 while d.even?
y = b.to_bn.mod_exp(d, self)
while d != n && y != n && y != 1
y = y.to_bn.mod_exp(2, self)
d <<= 1
end
return false if y != n && d.even?
true
end
# Returns true if +self+ is a prime number, else returns false.
def prime?
return true if [2, 3, 5, 7].include? self
return false unless self > 1 and 210.gcd(self) == 1
Prime::A014233.each do |pair|
ix, mx = pair
return false unless miller_rabin_test(ix)
break if self < mx
end
self == 41 ? true : miller_rabin_test(41)
end
end
Here are some timing results on a 2.3 GHz Lenovo Intel I5 laptop with a 32-bit Linux distro.
2.3.1 :103 > def tm; s=Time.now; yield; Time.now-s end
=> :tm
2.3.1 :104 > (10**1700 + 469).prime?
=> true
2.3.1 :105 > (10**1700 + 699).prime?
=> true
2.3.1 :106 > tm{ (10**1700 + 469).prime? }
=> 4.885875503
2.3.1 :107 > tm{ (10**1700 + 699).prime? }
=> 4.808029537
On a 3.5 GHz I7 System76 laptop with a 64-bit Linux distro the times for both were ~1.05secs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jzakiya's miller_rabin_test method could use at least one tweak at the end, apply De Morgan's law.
def miller_rabin_test(b)
return self >= 2 if self <= 3
# I would avoid creating a temp Array to check only 2 cases
return false unless [1,5].include?(self % 6)
n = d = self - 1 # self - 1 is pred, perhaps less readable
d >>= 1 while d.even?
y = b.to_bn.mod_exp(d, self)
while d != n && y != n && y != 1
y = y.to_bn.mod_exp(2, self)
d <<= 1
end
#return false if y != n && d.even?
#true
y == n || d.odd?
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I like the simplification at the end.
I used the array because it's visually simpler to see what the test is, but it can be replaced with:
return false unless [1, 5].include?(self % 6)
return false unless 6.gcd(self) == 1
6 = 2*3 is the P3 Prime Generator (PG) modulus, so for any number > 3, the residue n mod 6
has to be either 1 or 5 (coprimes to 6), which is equivalent to directly testing if the residue of n mod 6
is coprime to 6. The math for all of this is explained in my Primes-Utils Handbook.
Actually you can eliminate the first 2 tests for small primes (since prime?
tests for them too) if the method miller_rabin_test
is strictly to be used in prime?
, but since (as written) it's a public method of Integer
it's probably best to keep them, as they don't hurt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code here modifies the original prime?
implementation.
It first determines which is the smallest witness range for the number.
It then creates an array of all the witnesses for that range.
It then uses those witnesses to perform the miller_rabin_test
on.
The method prime1?
shows the form of the implementation.
Its performance is the same as the original prime?
implementation.
However, now that the witnesses are found first we can do prime2?
.
Using the parallel
gem, it can now perform the miller-rabin tests in parallel,
with a significant speedup for very large numbers, and no degradation in
performance for small numbers.
Below are average typical times (in seconds) with Ruby 2.3.1.
2.3.1 > def tm; s = Time.now; yield; Time.now-s end
2.3.1 > n = 10**2500; tm{ (n + 11859).prime? } # et al
system: 64-bit Linux
laptop: System76 Gazelle, 3.5GHz I7-6700HQ cpu, 4 cores, 8 threads
(10**2500 + 11859) (10**2500 + 13671)
prime? 3.16 3.19
prime1? 3.16 3.19
prime2? 0.95 0.96
system: 32-bit Linux
laptop: Lenovo V570, 2.3GHz I5-2410M cpu, 2 cores, 4 threads
(10**2500 + 11859) (10**2500 + 13671)
prime? 13.6 13.6
prime1? 13.6 13.6
prime2? 7.5 7.5
Below is the whole file.
require 'openssl'
require 'parallel'
class Integer
# Returns true if +self+ passes Miller-Rabin Test on +b+
def miller_rabin_test(b) # b is a witness to test with
return self >= 2 if self <= 3
return false unless 6.gcd(self) == 1
n = d = self - 1
d >>= 1 while d.even?
y = b.to_bn.mod_exp(d, self) # y = (b**d) mod self
while d != n && y != n && y != 1
y = y.to_bn.mod_exp(2, self) # y = (y**2) mod self
d <<= 1
end
y == n || d.odd?
end
# Returns true if +self+ is a prime number, else returns false.
def prime?
return true if [2, 3, 5, 7].include? self
return false unless self > 1 and 210.gcd(self) == 1
Prime::A014233.each do |pair|
ix, mx = pair
return false unless miller_rabin_test(ix)
break if self < mx
end
self == 41 || miller_rabin_test(41)
end
def prime1?
return true if [2, 3, 5, 7].include? self
return false unless self > 1 and 210.gcd(self) == 1
witnesses = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]
val = Prime::A014233.values.sort.detect {|v| v > self}
witnesses.select! { |p| p <= Prime::A014233.key(val) } if val
witnesses.each { |p| return false unless miller_rabin_test(p) }
true
end
def prime2? # perform miller-rabin tests in parallel
return true if [2, 3, 5, 7].include? self
return false unless self > 1 and 210.gcd(self) == 1
witnesses = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]
val = Prime::A014233.values.sort.detect {|v| v > self}
witnesses.select! { |p| p <= Prime::A014233.key(val) } if val
Parallel.each(witnesses) { |p| return false unless miller_rabin_test(p) }
true
end
end
class Prime
# https://oeis.org/A014233
#
# Smallest odd number for which Miller-Rabin primality test
# on bases <= n-th prime does not reveal compositeness.
#
A014233 = {
2 => 2_047,
3 => 1_373_653,
5 => 25_326_001,
7 => 3_215_031_751,
11 => 2_152_302_898_747,
13 => 3_474_749_660_383,
17 => 341_550_071_728_321,
19 => 341_550_071_728_321,
23 => 3_825_123_056_546_413_051,
29 => 3_825_123_056_546_413_051,
31 => 3_825_123_056_546_413_051,
37 => 318_665_857_834_031_151_167_461
}
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not that familiar with prime numbers, so I can only hope to help with the implementation.
Your default prime?
could be simplified, but wouldn't make such a great impact.
def prime?
return true if [2, 3, 5, 7].include? self
return false unless self > 1 and 210.gcd(self % 210) == 1
Prime::A014233.each do |ix, mx| # Remove pair, use (ix, mx) directly
return false unless miller_rabin_test(ix)
break if self < mx
end
self == 41 || miller_rabin_test(41) # Simplify return value
end
With your prime1?
and prime2?
there is more room to make a significant change.
There is a typo in prime2
, comma instead of dot for val = Prime::A014233.values.sort,detect {|v| v > self}
.
def prime1?
return true if [2, 3, 5, 7].include? self
return false unless self > 1 and 210.gcd(self % 210) == 1
witnesses = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]
# Isn't Prime::A014233.values already returning in the defined order in recent Ruby versions?
# Otherwise Prime::A014233.values.sort! could be stored in a constant
# array.sort! can be used here
# Some values are tested twice as they repeat in Prime::A014233,
# perhaps a constant could help here
val = Prime::A014233.values.sort!.detect {|v| v > self}
# Prime::A014233.key(val) is a constant inside the iterator, calculate it once outside
if val
# Since hash.key is used we could flip key/values to take more advantage of the Hash,
# but then keys would collide, are repeated values needed?
# Prime::A014233.key(3_825_123_056_546_413_051) should return one of [23,29,31] or 23 is enough for example?
val = Prime::A014233.key(val)
witnesses.select! { |p| p <= val }
end
# Simplify return value for the serial version, return false if any miller rabin test fails, otherwise true
witnesses.all? { |p| miller_rabin_test(p) }
end
Edit
After some time I noticed that val
and its respective key are used.
If hash.each
always iterates the key value pairs in the same order they were defined (1.9+ behavior, not sure if this will hold for future versions) I believe the following code is equivalent.
Otherwise hash.sort_by
could be used to force a sort by value.
def prime1?
return true if [2, 3, 5, 7].include? self
return false unless self > 1 and 210.gcd(self % 210) == 1
witnesses = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]
# Prime::A014233.sort_by {|k,v| v}.each do |k,v|
Prime::A014233.each do |k,v|
if v > self
witnesses.select! { |p| p <= k }
break
end
end
witnesses.all? { |p| miller_rabin_test(p) }
end
When I was in bed I also realized I could do Actually, your suggestions for First, my initial modifications of With So I separated out the distinct functional steps so they can be performed independently. The goal of Ah, but we can do better. The original design has some unnecessary dependencies we can eliminate to make the code The design uses a specific set of ranges/witnesses to provide deterministic results over those The way the Prime::A014233 hash is constructed, each larger range is dependent on all the The fix is to create the hash so that the keys are the ranges whose values are arrays of In the code for Let's break this down.
Then we do In But I can even make Since I'm creating an array of consecutive So Thus, I think
|
I am glad that I could help in minor details and eventually motivated this discussion. |
Been busy, couldn't respond immediately. Regarding test passing, all my versions of But the crux of the pull request is to improve the performance of the Fortunately, the current method for However, this version, which uses the P3 Strictly-Prime Prime Generator (SP PG),
The P7 SP PG is even faster (for increasing numbers) but the code is not as compact. Below are some timing comparisions for 3 progressively larger primes for 2.3.1.
So, just making this minor change to This pull request uses the Miller-Rabin (MR) test for Repeatedly run this test below (for small numbers) with
From my testing, using SP PGs are faster than MR for 'small' numbers, and 100% accurate, This suggests that if you want the best of both worlds (100% accuracy and speed) in one Below are additional simplifications to the methods
Here are all my modifications to the pull requester's code.
Now you can do, very fast, and for very large numbers, things like:
There are still code and performance improvements that can be made to the other |
This page shows numbers up to 71. ψ20 meaning [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71 ] base numbers. Prime::A014233 can extend
|
It seems to have a conflict now. Could you rebase this from master? |
Let me close this as it has not been updated for a while. Please reopen this after resolving conflicts. Thanks. |
Rationale
To me, integer-without-limit is one of the greatest features of Ruby. I am currently working on my own implementation of arbitrary precision number system (https://github.com/dankogai/swift-pons) and ruby has been my sensei.
That is why I am disappointed to find
prime.rb
is pretty darn impractical, even for such "small" numbers below the 64-bit boundary. Consider this:M61 is well below an ordinary, fixed, 64-bit integer can hold.
This patch makes
prime.rb
a little more practical by:.prime?
base upon Miller-Rabin primarity testuint64_t
max..next_prime
and.prev_prime
which returns the (next|previous) prime.Prime::NextPrimeGenerator
which makes use of.next_prime
.vs. OpenSSL::BN
Like current
prime.rb
, this patch is by no means to replaceOpenSSL::BN.prime?
. For very large numbersOpenSSL::BN
is still faster. But for numbers below 32-bit limit this patch is faster. And for numbers between 32-bit limit and 64-bit limit, its performance is okay.Conclusion
IMHO the gap between
prime.rb
andOpenSSL::BN
is now unacceptably too large. It was acceptable when native integers were only 32-bit wide. But it is 2016 and even your phone may be using 64-bit int..prime?
should return instantly at least within 64-bit range.Dan the Amateur Rubyist