半秒求一亿内的素数:经典筛选法&线性筛选法的介绍、改进、实现与性能分析

埃拉托斯特尼筛法,简称埃氏筛,也有人称素数筛。这是一种简单且历史悠久的筛法,用来找出一定范围内所有的素数。
本文主要内容为四种筛选法的原理介绍和具体实现(C++),并对比它们之间的性能差距。这四种筛选法分别是:经典筛选法、采用了欧拉函数的线性筛选法,以及个人对这两种筛选法的改进

经典筛选法

原理简介

关于经典筛选法,维基百科上的动图能很好地解释其原理。先用2去筛,即把2留下,把2的倍数剔除掉;再用下一个素数,也就是3筛,把3留下,把3的倍数剔除掉;接下去用下一个素数5筛,把5留下,把5的倍数剔除掉;不断重复下去…

具体实现

#define MAX 100000000  // 求一亿范围内的素数
long prime_numbers[6000000], n = 0;  // 一亿内有500多万个素数

void eratosthenes() {
    static bool not_prime_flag[MAX];  // 假设所有数都为素数
    for (long num = 2; num < MAX; num++) {
        if (!not_prime_flag[num]) {
            prime_numbers[n++] = num;
            for (long times = 2; times * num < MAX; times += 1) {
                not_prime_flag[times * num] = true;  // 将素数的倍数剔除掉
            }
        }
    }
}

线性筛选法

原理简介

经典筛选法存在一个性能问题:某些合数会被剔除多次。比如6,即会作为2的倍数被剔除一次,又会作为3的倍数被剔除一次。那么有没有方法减少甚至去除重复计算呢?有的,采用了欧拉公式的线性筛选法可以避免重复计算。由于所有的合数都能被至少一个素数整除,所以只要我们找到一个合数的最小质因子就剔除掉这个合数,那就不会出现重复计算了。具体的原理可以参考下列文章:

具体实现

#define MAX 100000000  // 求一亿范围内的素数
long prime_numbers[6000000], n = 0;  // 一亿内有500多万个素数

void sieve_euler() {
    static bool not_prime_flag[MAX];  // 假设所有数都为素数
    for (long num = 2; num < MAX; num++) {
        if (!not_prime_flag[num]) {
            prime_numbers[n++] = num;
        }
        for (long pi = 0; pi < n && num * prime_numbers[pi] < MAX; pi++) {
            not_prime_flag[num * prime_numbers[pi]] = true;  // 用不同的素数组合乘积剔除合数
            if (num % prime_numbers[pi] == 0) {
                break;
            }
        }
    }
}

筛选法的改进

我们知道,除了2之外,所有的偶数都是合数。那么只要在进行筛选时只对奇数进行判断,那么就会有效减少计算量。同时,我们可以不为偶数的素数标记分配内存,减少内存消耗量。

经典筛选法的改进

#define MAX 100000000  // 求一亿范围内的素数
long prime_numbers[6000000] = {2}, n = 1;  // 一亿内有500多万个素数

void eratosthenes_plus() {
    static bool not_prime_flag[MAX / 2];  // 不为偶数分配内存
    for (long i = 0, num = i * 2 + 3; num < MAX; num = ++i * 2 + 3) {  // 只考虑3之后的奇数
        if (!not_prime_flag[i]) {
            prime_numbers[n++] = num;
            for (long times = 3; times * num < MAX; times += 2) {
                not_prime_flag[(times * num - 3) / 2] = true;  // 数字转下标
            }
        }
    }
}

线性筛选法的改进

#define MAX 100000000  // 求一亿范围内的素数
long prime_numbers[6000000] = {2}, n = 1;  // 一亿内有500多万个素数

void sieve_euler_plus() {
    static bool not_prime_flag[MAX / 2] = {};  // 不为偶数分配内存
    for (long i = 0, num = i * 2 + 3; num < MAX; num = ++i * 2 + 3) {  // 只考虑3之后的奇数
        if (!not_prime_flag[i]) {
            prime_numbers[n++] = num;
        }
        for (long pi = 1; pi < n && num * prime_numbers[pi] < MAX; pi++) {
            not_prime_flag[(num * prime_numbers[pi] - 3) / 2] = true;  // 用不同的素数组合乘积剔除合数
            if (num % prime_numbers[pi] == 0) {
                break;
            }
        }
    }
}

性能对比

测试平台:Intel Core i3 8100、Ubuntu LTS 18.04、C++11、c++ Compiler 7.3.0
测试方法为计算100000000内所有的素数
下表中函数运行时间为10次运行时间消耗的平均值

函数名称备注函数运行时间(s)
eratosthenes()经典筛选法2.2840
sieve_euler()线性筛选法1.1117
eratosthenes_plus()经典筛选法(改进)1.0774
sieve_euler_plus()线性筛选法(改进)0.6312

  • 由于两个改进筛选法没有为偶数分配内存,所以在我的测试平台上,其对应的两个函数的内存占用相较于未改进的筛选法减少了一半(47.5MB)。
  • 同时在测试过程中我发现一个奇怪的问题:假设eratosthenes_plus函数中为偶数分配内存,那么加、减、乘的操作次数就会大大降低(因为不需要在数字和下标之间进行转换),降低的规模甚至是以亿计的,但实际上其运行时间反而增长到了1.2s左右。个人猜测原因可能是编译器的主动优化,在这里就不深入研究了……

寄存器优化

偏个题,假设使用速度比内存快得多的CPU寄存器来存储会频繁用到的numtimesipi这些变量,函数运行时间会有多大提升呢?

函数名称备注函数运行时间(s)提升幅度
eratosthenes()经典筛选法1.884921.17%
sieve_euler()线性筛选法1.04836.05%
eratosthenes_plus()经典筛选法(改进)0.887721.37%
sieve_euler_plus()线性筛选法(改进)0.562912.13%


可以看出寄存器优化对性能的提升还是比较明显的,最高可以达到20%。

引用

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页