As user1118321 notes, there's no need to write your own string-to-integer conversion. Just use one of the standard library functions provided for this purpose, such as atoi()
(simple) or strtol()
/ strtoul()
(provides better error handling). This also lets you get rid of the klugy use of -1 / -2 as error indicators.
Also, as suggested by Peter Cordes while I was writing this, don't define your own bool
type and true
/false
constants. Just #include <stdbool.h>
instead.
Your main loop uses what, to me, is a funny brace style:
for (int i = 2; i <= max_sqrt; i++) {
if (list[i])
for (int j = i*i; j <= max; j += i)
list[j] = false;
}
For consistency, I'd expect either all of the for
and if
statements to have braces, or (if you really love braceless loops) none of them. Personally, I prefer to always use braces unless the entire loop / conditional is on one single line. That is, I'd prefer to write this as:
for (int i = 2; i <= max_sqrt; i++) {
if (list[i]) {
for (int j = i*i; j <= max; j += i) {
list[j] = false;
}
}
}
If I really wanted to minimize the line count for some reason, I might squeeze the inner loop onto one line and replace the if
block with an if (...) continue
statement, like this:
for (int i = 2; i <= max_sqrt; i++) {
if (!list[i]) continue;
for (int j = i*i; j <= max; j += i) list[j] = false;
}
In this particular case, though, I'd prefer the longer version. The one-line style is fine for trivial loops or conditionals, where the whole thing can be understood at a glance. But your inner loop, with its unusual stepping and starting point, is complex enough that it IMO benefits from a bit more verbosity.
You could make your code twice as fast by treating 2 as a special case and skipping all even numbers, like this:
for (int i = 3; i <= max_sqrt; i += 2) {
if (list[i]) {
for (int j = i*i; j <= max; j += 2*i) {
list[j] = false;
}
}
}
Note that now this code doesn't touch the even-numbered elements of list
at all, so you can also reduce your memory use by not including them in your sieve at all!
To avoid cluttering your code with index adjustments, and to reduce the risk of off-by-one errors, it may be useful to define a macro (or an inline function) that takes care of the indexing, e.g. like this:
/* test whether the odd number idx is in the sieve */
static inline bool is_in_sieve(bool *sieve, int idx) {
return sieve[idx / 2];
}
/* remove the odd number idx from the sieve */
static inline void remove_from_sieve(bool *sieve, int idx) {
sieve[idx / 2] = false;
}
which you could then use in your code like this:
for (int i = 3; i <= max_sqrt; i += 2) {
if (is_in_sieve(list, i)) {
for (int j = i*i; j <= max; j += 2*i) {
remove_from_sieve(list, j);
}
}
}
In principle, you could save a further 33% of your runtime and memory use by also treating 3 as a special case and leaving all multiples of 3 out of your sieve, and so on, but this process (which naturally leads to wheel factorization) starts hitting diminishing results pretty quickly.
A more practically useful trick would be to get rid of the wasted space in your sieve array by using only a single bit for each number. This will reduce your memory use by a factor of (at least) 8, at the cost of a small runtime increase.
For example, you could make your sieve a char
array of (max + 15) / 16
elements, initialize them all with 0xFF
, and rewrite the inline helper functions as:
/* test whether the odd number idx is in the sieve */
static inline bool is_in_sieve(char *sieve, int idx) {
return sieve[idx / 16] & (1 << (idx % 16 / 2));
}
/* remove the odd number idx from the sieve */
static inline void remove_from_sieve(char *sieve, int idx) {
sieve[idx / 16] &= ~(1 << (idx % 16 / 2));
}
The actual sieving loop doesn't need to be changed in any way, since all the array accesses are already encapsulated by these two helper functions!
Also, it might be simpler and more efficient to just let the sieve array be initialized to all zeros (which we can do easily with calloc()
) and set the bits that correspond to numbers not in the sieve. Again, we can do this simply by changing the helper functions:
/* test whether the odd number idx is in the sieve */
static inline bool is_in_sieve(char *sieve, int idx) {
return !( sieve[idx / 16] & (1 << (idx % 16 / 2)) );
}
/* remove the odd number idx from the sieve */
static inline void remove_from_sieve(char *sieve, int idx) {
sieve[idx / 16] |= (1 << (idx % 16 / 2));
}
I'd be tempted to remove your arbitrary hard-coded MAX
limit, since it's just that — arbitrary. If your computer can handle larger sieves, why not let it?
On the other hand, removing the upper limit does mean that we would need to check for a bunch of problems that could arise if max
was too large:
- If the type of
max
was wider than size_t
, the argument to malloc()
might overflow.
- If
max
was too large to be exactly representable as a double
, the value of sqrt(max)
might be slightly off.
- If
max > INT_MAX - 2*sqrt_max
, then j += 2*i
might overflow. If j
is a signed integer, this is undefined behavior; if we make j
unsigned, it "merely" leads to an infinite loop.
Out of those, the last limit is probably the most restrictive, at least as long as max
is of type int
. Off the top of my head, I'm not sure if size_t
is guaranteed not to be narrower than int
, but it would be a very perverse system indeed where that wasn't the case. And since an IEEE double
can store up to 53 bit integers accurately, and since (int)sqrt(x)
only has about half as many bits as x
, we're unlikely to see any floating-point accuracy issues unless max
is greater than 2100 or so. While that's technically possible e.g. on a DSP with 128-bit int
s, there are no computers with enough memory for a sieve that big. And even if there were, you'd be long dead before the CPU could even finish counting from 1 up to 2100.
So as long as we check that max <= INT_MAX - 2*sqrt(INT_MAX)
, we should probably be fine. If you really felt paranoid, you could also check that max / 16 < (size_t)-1
and that max == (int)(double)max
, but that's almost certainly overkill unless you're using this code to control a nuclear reactor or something. And I say that as someone who tends to be very heavily into defensive coding.
So, with all those changes (and a bunch of other minor tweaks), here's how I'd rewrite your code:
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <limits.h>
#include <math.h>
#include <errno.h>
/* test whether the odd number idx is in the sieve */
static inline bool is_in_sieve(char *sieve, int idx) {
return !( sieve[idx / 16] & (1 << (idx % 16 / 2)) );
}
/* remove the odd number idx from the sieve */
static inline void remove_from_sieve(char *sieve, int idx) {
sieve[idx / 16] |= (1 << (idx % 16 / 2));
}
int main(int argc, char **argv) {
/* it's technically possible that argc == 0 */
char *program_name = (argc > 0 ? argv[0] : "sieve");
if (argc != 2) {
fprintf(stderr, "Usage: %s <max>\n"
"\tmax: number to find primes up to\n",
program_name);
return EXIT_FAILURE;
}
/* carefully parse the input number */
errno = 0;
char *tail;
long max_long = strtol(argv[1], &tail, 10);
if (errno || *tail != '\0' || max_long < 0 || max_long > INT_MAX) {
fprintf(stderr, "%s: invalid maximum \"%s\"\n", program_name, argv[1]);
return EXIT_FAILURE;
}
/* if we were lazy and careless, we'd just do max = atoi(argv[1]) */
int max = max_long;
/* there are no primes smaller than 2 */
if (max < 2) return EXIT_SUCCESS;
/* make sure that j += 2*i and the sieve allocation cannot overflow */
if (max > INT_MAX - 2*(int)sqrt(INT_MAX) || max / 16 >= (size_t)-1) {
fprintf(stderr, "%s: maximum %d too large\n", program_name, max);
return EXIT_FAILURE;
}
/* allocate the sieve array of (max+1)/16 bytes, rounded up */
char *sieve = calloc(max / 16 + 1, 1);
if (!sieve) {
fprintf(stderr, "%s: failed to allocate %d element sieve\n", program_name, max);
return EXIT_FAILURE;
}
int max_sqrt = sqrt(max);
for (int i = 3; i <= max_sqrt; i += 2) {
if (is_in_sieve(sieve, i)) {
for (int j = i*i; j <= max; j += 2*i) {
remove_from_sieve(sieve, j);
}
}
}
puts("2"); /* "2 is the oddest prime, because it's even" */
for (int i = 3; i <= max; i += 2) {
if (is_in_sieve(sieve, i)) printf("%d\n", i);
}
free(sieve);
return EXIT_SUCCESS;
}
Try it online!
On my old laptop, this takes about 1 minute 20 seconds and about 122 MiB of memory to generate all primes up to 2,000,000,000 (and to print them to /dev/null
). Out of that 122 MiB, the sieve array takes up a little over 119 MiB, with the rest presumably being miscellaneous overhead.
If you want to go even higher, then at some point switching to a segmented sieve could indeed be a good idea.
i*i
is correct - all smaller multiples ofi
have already been eliminated. Consider you are eliminating multiples of 5 - there is no point eliminating 10 (even), 15 (multiple of three), or 20 (even). \$\endgroup\$