Over the years I’ve seen many C# sieves and despite their varying internals all that I have seen invariably are a static method that returns a List<int>
, perhaps with the occasional rare exception of an Enumerable<int>
. This is fine for small collections. Granted relatively speaking for some people a 1 million item list is small. But if dealing with very large upper limits, say int.MaxValue
, you could run into an out-of-memory exception fetching over 105 million items.
So I wrote a sieve class that has these highlights:
- Internally uses
BitArray
to track odd numbers only. - Class implements IEnumerable with custom enumerator to walk over
BitArray
to findtrue
bits. - Lazy evaluation.
- Methods such as
IsPrime(number)
andFindNthPrime(sequence)
.FindNthPrime(-1)
returns last prime found, equivalent toFindNthPrime(Count)
. - On-demand count is performed in parallel. Counting also caches
subtotals in ranges of 250K bits to boost
FindNthPrime
. ToNumber
andToIndex
functions add context and clarity for cleaner code.- And for those who still only want to get a
List<int>
returned, I included a convenient methodGetPrimes
.
Usings:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
SieveOfEratosthenes:
public class SieveOfEratosthenes : IEnumerable
{
public static SieveOfEratosthenes CreateInstance(int upperLimit)
{
if (upperLimit < 2)
{
throw new ArgumentException("Upper Limit be must greater than or equal to 2.");
}
return new SieveOfEratosthenes(upperLimit);
}
private SieveOfEratosthenes(int upperLimit)
{
_upperLimit = upperLimit;
}
// In case you were wondering ... the maximum length of _bits will be 1,073,741,823 when _upperLimit = int.MaxValue.
// That's over 1 billion bits which comes out to be 134,217,728 bytes.
private BitArray _bits = null;
private int _knownCount = 0;
private int _upperLimit = 2;
private const int _offset = 3;
// Also in case you were wondering ... the maximum number of entries in _countsCache SortedList:
// rangeSize 500K produces 2148 entries
// rangeSize 250K produces 4096 entries
// The SortedList is only populated when needed.
private SortedList<int, int> _countsCache = new SortedList<int, int>();
private const int _rangeSize = 250000;
// Tried methods, delegates, and inline code and all performed equally.
public static Func<int, int> ToNumber = delegate(int index) { return (2 * index) + _offset; };
private static Func<int, int> ToIndex = delegate(int number) { return (number - _offset) / 2; };
private void CheckBits()
{
if (!HasSieveBeenRun)
{
RunSieve();
}
}
public void CheckCount()
{
if (_knownCount > 0) return;
_countsCache = new SortedList<int, int>();
_knownCount = 1;
if (_upperLimit == 2)
{
return;
}
CheckBits();
// Initialize sorted list in sorted order.
// I create the list before the parallel loop so the parallel writes won't be in conflict later.
for (int i = 0; i < _bits.Length; i += _rangeSize)
{
_countsCache.Add(i, 0);
}
var ranges = Partitioner.Create(0, _bits.Length, _rangeSize);
Parallel.ForEach(ranges, range =>
{
_countsCache[range.Item1] = CountPrimesRange(range.Item1, range.Item2);
});
foreach (var entry in _countsCache)
{
_knownCount += entry.Value;
}
}
public int Count
{
get
{
// Lazy evaluation.
// If _upperLimit = int.MaxValue, we also get SLUGGISH evaluation.
CheckCount();
return _knownCount;
}
}
private int CountPrimesRange(int inclusiveStart, int exclusiveEnd)
{
var subtotal = 0;
for (int i = inclusiveStart; i < exclusiveEnd; i++)
{
if (_bits[i])
{
subtotal++;
}
}
return subtotal;
}
public int Estimate
{
get { return (int)(_upperLimit / Math.Log(_upperLimit)); }
}
public int FindNthPrime(int sequence)
{
// Finding the nth prime may take time to Count first, if it hasn't already been done.
if (sequence == 1)
{
return 2;
}
if (sequence == -1)
{
sequence = Count;
}
if ((sequence < 1) || (sequence > Count))
{
throw new ArgumentException(string.Format("Sequence must be between 1 and Known Count of {0} inclusively.", Count.ToString("N0")));
}
var tally = FindQuickTally(sequence);
var counter = tally.Item2;
var prime = 0;
for (int i = tally.Item1; i < _bits.Length; i++)
{
if (_bits[i])
{
if (++counter == sequence)
{
prime = ToNumber(i);
break;
}
}
}
return prime;
}
private Tuple<int, int> FindQuickTally(int sequence)
{
var index = 0;
var totalRangeStart = 1; // remember to include 2 (our first prime)
foreach (var entry in _countsCache)
{
index = entry.Key;
var totalRangeEnd = totalRangeStart + entry.Value;
if (sequence <= totalRangeEnd)
{
break;
}
totalRangeStart = totalRangeEnd;
}
return new Tuple<int, int>(index, totalRangeStart);
}
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)GetEnumerator();
}
public EratosthenesEnumerator GetEnumerator()
{
CheckBits();
return new EratosthenesEnumerator(_bits);
}
public static IList<int> GetPrimes(int upperLimit)
{
// This is included for people who like getting just a list of primes back.
// Worst Case: _upperLimit = int.MaxValue
// _bits is 134,217,728 bytes or 128 MB.
// The output list is 105,097,565 items needing 420,390,260 bytes or 401 MB.
// All in all, you need 529 MB RAM for the worst case.
var sieve = CreateInstance(upperLimit);
sieve.RunSieve();
var list = new List<int>(capacity: sieve.Estimate);
try
{
foreach (var prime in sieve)
{
list.Add(prime);
}
}
catch (OutOfMemoryException)
{
throw new Exception("Too many primes caused an out of memory exception. Consider iterating over sieve like 'foreach (var prime in sieve)' instead.");
}
return list;
}
public bool HasSieveBeenRun
{
get { return _bits != null; }
}
public bool IsComposite(int number)
{
return !IsPrime(number);
}
public bool IsPrime(int number)
{
if ((number < 2) || (number > _upperLimit))
{
throw new ArgumentException(string.Format("Number must be between 2 and Upper Limit of {0} inclusively.", _upperLimit.ToString("N0")));
}
if (number % 2 == 0) return number == 2;
CheckBits();
return _bits[ToIndex(number)];
}
public void RunSieve()
{
if (HasSieveBeenRun) return;
_knownCount = 0;
if (_upperLimit == 2)
{
_bits = new BitArray(0);
return;
}
_bits = new BitArray(ToIndex(_upperLimit) + 1, defaultValue: true);
var upperSqrtIndex = ToIndex((int)Math.Sqrt(_upperLimit));
for (var i = 0; i <= upperSqrtIndex; i++)
{
// If this bit has already been turned off, then its associated number is composite.
if (!_bits[i]) continue;
var number = ToNumber(i);
// Okay, so number is now known to be prime.
// However, any multiples of number are composite and their respective bits should be turned off.
for (var j = ToIndex(number * number); (j > i) && (j < _bits.Length); j += number)
{
_bits[j] = false;
}
}
}
public int UpperLimit { get { return _upperLimit; } }
}
EratosthenesEnumerator
public class EratosthenesEnumerator : IEnumerator
{
private BitArray _bits = null;
// Enumerators are positioned before the first element until the first MoveNext() call.
private int _primePosition = -1;
private int _bitPosition = -1;
public EratosthenesEnumerator(BitArray bits)
{
_bits = bits;
}
public bool MoveNext()
{
_primePosition++;
if (_primePosition > 0)
{
var found = -1;
for (var i = _bitPosition + 1; i < _bits.Length; i++)
{
if (_bits[i])
{
found = i;
break;
}
}
_bitPosition = (found >= 0) ? found : _bits.Length;
}
return (_primePosition >= 0) && (_bitPosition < _bits.Length);
}
public void Reset()
{
_primePosition = -1;
_bitPosition = -1;
}
object IEnumerator.Current
{
get
{
return Current;
}
}
public int Current
{
get
{
try
{
if (_primePosition == 0)
{
return 2;
}
return SieveOfEratosthenes.ToNumber(_bitPosition);
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
}
Not Interested In
This class uses a BitArray
and int
. PERIOD.
Don’t bother suggesting that I could perform parallel writes with a ConcurrentList<bool>
. I am not interested in that here. Yet I would be happy to read a new, original thread created by you where you use ConcurrentList
.
Don’t bother suggesting that I could use larger integral types. I am not interested in that here. Yet I would be happy to read a new, original thread created by you where you use uint
, long
, ulong
, or even BigInteger
.
The cached counts is a feature that I refuse to remove but am open to improvements. It’s called on-demand, trades-off a small but acceptable amount of memory, and really boosts performance of FindNthPrime
.
Concerns
This was my first time to use a function delegate, as well as a custom enumerator. The ToNumber
function is declared public static
in the SieveOfEratosthenes
class, and for the sake of DRY is referenced within the EratosthenesEnum
class. Since this was my first time to use either, I don’t know if it’s a proper use or not.
ToIndex
is the yin to ToNumber
‘s yang but since the enumerator doesn’t use it, I have its access as private. Should it stay private or should it be public like it twin ToNumber
?
Usage
I tried to make the usage feel natural. Consider:
var sieve = SieveOfEratosthenes.CreateInstance(limit);
sieve.RunSieve();
foreach (var prime in sieve)
{
/* do something */
}
sieve.CheckCount();
Console.WriteLine("Count of primes {0}", sieve.Count);
But since the class implements IEnumerable
which calls RunSieve
if needed, and Count
calls CheckCheck
, this performs the same thing:
var sieve = SieveOfEratosthenes.CreateInstance(limit);
foreach (var prime in sieve)
{
/* do something */
}
Console.WriteLine("Count of primes {0}", sieve.Count);
The only reason I have RunSieve
and CheckCount
as public methods is for timing them individually with a Stopwatch
.
The sad thing is this is a nice class but the things that work best and fastest with it - for a reasonable upper limit - are the things that aren’t needed since you can simply output as a List<int>
. And the things that were put in to address really large upper limits are the things that will still be sluggish.
Nonetheless with this class, I can at least iterate over all of the int
primes without an out-of-memory exception, which is something I can’t do with the other sieves that I’ve seen.
OutOfMemoryException
doesn't usually come from having too many primes, but from using abool[]
(or evenint[]
!) instead of a BitArray. There are a little over 105 million primes below int.MaxValue. Thatint[]
will take up about 400Mb of memory. Still doable. Abool[]
of length int.MaxValue however, will take up 2Gb of memory. That can be a problem. \$\endgroup\$int.MaxValue
since the bit array also uses memory. \$\endgroup\$IEnumerable<int>
and generate primes only when needed. If someone wants to put them in aList
, that's their responsibility. \$\endgroup\$