This question is a revision of Parallel sieve of Eratosthenes. The goal is to implement a sieve of Eratosthenes with parallel strikes out from the boolean array. I tried to fix the data races and all the threading-related errors as well as to add some of the ideas from the previous answers. Now, the implementation works as follows:
- Compute the prime numbers \$ p \$ such as \$ p <= \sqrt{n} \$ thanks to a sequential sieve of Eratosthenes.
- Initialize a
std::vector<std::atomic<bool>>
withtrue
for indices between \$ 0 \$ and \$ n \$ (inclusive). - Compute the number of threads that should be used to concurrently strike out values from the vector. This number depends on the maximum number of concurrent threads allowed by the implementation and on the number of precomputed prime numbers.
- Spawn threads that will strike out the multiples of a given set of prime numbers from the boolean vector.
- Join the threads.
- Find the actual remaining prime numbers.
To differentiate between the sequential and parallel versions of the sieve, I had the function take an execution policy parameter first, inspired from the C++ parallelism TS N3960. The execution policy classes can be trivially implemented as such (I know... we shouldn't add anything new to std::
):
namespace std
{
namespace parallel
{
struct sequential_execution_policy {};
struct parallel_execution_policy {};
struct vector_execution_policy {};
constexpr sequential_execution_policy seq = sequential_execution_policy();
constexpr parallel_execution_policy par = parallel_execution_policy();
constexpr vector_execution_policy vec = vector_execution_policy();
}}
Here is the sequential version of sieve_eratosthenes
. In the end, I did not make strike_out_multiples
a lambda since I use it multiple times.
template<typename Integer, typename T>
void strike_out_multiples(Integer n, std::vector<T>& vec)
{
for (Integer i = n*2u ; i < vec.size() ; i += n)
{
vec[i] = false;
}
}
template<typename Integer>
auto sieve_eratosthenes(std::parallel::sequential_execution_policy, Integer n)
-> std::vector<Integer>
{
if (n < 2u)
{
return {};
}
std::vector<char> is_prime(n+1u, true);
// Strike out the multiples of 2 so that
// the following loop can be faster
strike_out_multiples(2u, is_prime);
// Strike out the multiples of the prime
// number between 3 and end
auto end = static_cast<Integer>(std::sqrt(n));
for (Integer n = 3u ; n <= end ; n += 2u)
{
if (is_prime[n])
{
strike_out_multiples(n, is_prime);
}
}
std::vector<Integer> res = { 2u };
for (Integer i = 3u ; i < is_prime.size() ; i += 2u)
{
if (is_prime[i])
{
res.push_back(i);
}
}
return res;
}
And now, here is the new parallel version of sieve_eratosthenes
:
template<typename Integer>
auto sieve_eratosthenes(std::parallel::parallel_execution_policy, Integer n)
-> std::vector<Integer>
{
if (n < 2u)
{
return {};
}
// Only the prime numbers <= sqrt(n) are
// needed to find the other ones
auto end = static_cast<Integer>(std::sqrt(n));
// Find the primes numbers <= sqrt(n) thanks
// to a sequential sieve of Eratosthenes
const auto primes = sieve_eratosthenes(std::parallel::seq, end);
std::vector<std::atomic<bool>> is_prime(n+1u);
for (auto i = 0u ; i < n+1u ; ++i)
{
is_prime[i].store(true, std::memory_order_relaxed);
}
std::vector<std::thread> threads;
// Computes the number of primes numbers that will
// be handled by each thread. This number depends on
// the maximum number of concurrent threads allowed
// by the implementation and on the total number of
// elements in primes
std::size_t nb_primes_per_thread =
static_cast<std::size_t>(std::ceil(
static_cast<float>(primes.size()) /
static_cast<float>(std::thread::hardware_concurrency())
));
for (std::size_t first = 0u;
first < primes.size();
first += nb_primes_per_thread)
{
auto last = std::min(first+nb_primes_per_thread, primes.size());
// Spawn a thread to strike out the multiples
// of the prime numbers corresponding to the
// elements of primes between first and last
threads.emplace_back(
[&primes, &is_prime](Integer begin, Integer end)
{
for (std::size_t i = begin ; i < end ; ++i)
{
auto prime = primes[i];
for (Integer n = prime*2u ; n < is_prime.size() ; n += prime)
{
is_prime[n].store(false, std::memory_order_relaxed);
}
}
},
first, last);
}
for (auto& thr: threads)
{
thr.join();
}
std::vector<Integer> res = { 2u };
for (Integer i = 3u ; i < is_prime.size() ; i += 2u)
{
if (is_prime[i].load(std::memory_order_relaxed))
{
res.push_back(i);
}
}
return res;
}
And of course, an example main
:
int main()
{
auto primes = sieve_eratosthenes(std::parallel::par, 10000u);
for (auto prime: primes)
{
std::cout << prime << " ";
}
}
Is this code correct (I think that I have resolved the threading-related issues), and if so, how can I make it even better? I can think of two ideas from the previous question that I did not implement:
- Using a thread pool (still have to learn how to create one).
- Using some heuristic to decide when using multiple threads may be better than using only one thread. That heuristic would probably rely on the number of prime numbers handled by each thread.
Note: the code does not work with clang++ 3.5: it seems that libc++ std::vector<std::atomic<T>>(std::size_t count)
somehow tries to copy some std::atomic<T>
instances while it should not copy anything.
vector(size_type n, const value_type& val = value_type())
. The second argument (if not supplied) is defaulted; but it is also "copied" into every cell on the vector. It may work better if you use resize() as that value initialize the extra elements. \$\endgroup\$count
guarantees that no copies are made. \$\endgroup\$prime
across threads. But each thread must also access all write memoryis_prime
. I woud reverse that. There is no contention if all threads use all parts of read memoryprime
then divide the write memoryis_prime
across the threads. So each thread is writing to its own personal chunk of write memory. \$\endgroup\$std::thread::hardware_concurrency
returns0
if it cannot compute a value, which would cause a SIGFPE innb_primes_per_thread
. \$\endgroup\$