我已经编写了一个小的C程序来计算素数,现在尝试尽我所能优化代码。
在程序的第一版中,我正在检查一个数字是否为偶数(模2),如果是,我将继续到下一个数字。
在第二次修订中,我尝试通过将要检查的数字增加2来仅检查奇数是否为可能的质数(所以我将从3开始,然后依次检查5、7、9、11等)
我认为这样做会更快,因为我用代码削减了对模2的额外检查,并简单地用附加代码代替了它。 但是,令我惊讶的是,仅检查奇数的代码在大多数情况下的运行速度要比实现检查所有数字的实现慢一些。
这是代码(它包含我在注释的修订之间所做的更改,无论它说// CHANGE)
#include <stdio.h>
#include <stdbool.h>
#include <math.h>
unsigned long i = 3; //CHANGE No 1 - it was i = 2;
bool hasDivisor(unsigned long number)
{
//https://stackoverflow.com/questions/5811151/why-do-we-check-up-to-the-square-root-of-a-prime-number-to-determine-if-it-is-pr
unsigned long squareRoot = floor(sqrt(number));
//we don't check for even divisors - we want to check only odd divisors
//CHANGE No 2 - it was (number%2 ==0)
if(!((squareRoot&1)==0)) //thought this would boost the speed
{
squareRoot += 1;
}
for(unsigned long j=3; j <= squareRoot; j+=2)
{
//printf("checking number %ld with %ld \n", number, j);
if(number%j==0)
{
return true;
}
}
return false;
}
int main(int argc, char** argv)
{
printf("Number 2 is a prime!\n");
printf("Number 3 is a prime!\n");
while(true)
{
//even numbers can't be primes as they are a multiple of 2
//so we start with 3 which is odd and contiously add 2
//that we always check an odd number for primality
i++; //thought this would boost the speed instead of i +=2;
i++; //CHANGE No 3 - it was a simple i++ so eg 3 would become 4 and we needed an extra if(i%2==0) here
//search all odd numbers between 3 and the (odd ceiling) sqrt of our number
//if there is perfect division somewhere it's not a prime
if(hasDivisor(i))
{
continue;
}
printf("Number %ld is a prime!\n", i);
}
return 0;
}
我正在将Arch Linux x64与GCC版本8.2.1配合使用,并使用以下命令进行编译:
gcc main.c -lm -O3 -o primesearcher
尽管我也用O1和O2进行了测试。
我正在使用以下命令进行“基准测试”:
./primesearcher & sleep 10; kill $!
会运行程序10秒钟,并在这段时间内向终端输出素数,然后将其杀死。 我显然尝试过让程序运行更多时间(30、60和180秒),但结果大约是9/10的时间,这有利于版本检查偶数(模2版本在被杀死之前找到了更大的质数)
有人知道为什么会这样吗? 最终在代码方面可能出了什么问题?
答案 0 :(得分:1)
if(!((squareRoot&1)==0))
的代码比没有测试的要慢,因为它几乎没有好处。
请记住,对于大多数number
来说,由于number%j
测试早日返回,因此从未达到迭代限制。随着number
的增长,素数趋于变得稀有。
罕见的额外迭代并不能抵消测试的重复成本。
将!((squareRoot&1)==0)
与number%2 ==0
进行比较是moot。
当差异很小时,OP的测试速度方法容易出错:“在大多数情况下运行得稍微慢一点”表明不一致。
巨大的时间在printf()
中。为了比较主要的计算性能,需要消除I / O。
kill
也不够精确。
相反,形成一个循环,以在i
达到4,000,000,000之类的值时停止。
代码还有其他弱点:
unsigned long squareRoot = floor(sqrt(number));
可能会为大number
创建错误的根,这是由于将number
转换为double
时的舍入以及sqrt()
例程的不精确性。 OP的reference满足数学上的算法需求。然而,鉴于实际计算的局限性,这种C代码实现很容易失败。
相反,建议
// Prime test for all unsigned long number
bool isPrime(unsigned long number) {
if (number % 2 == 0) { // This can be eliminated if `number` is always odd.
return number == 2;
}
for (unsigned long j = 3; j <= number/j; j += 2) {
if (number%j == 0) {
return false;
}
}
return number > 2;
}
现代编译器看到number/j
时number%j
的成本通常为零,并且可以同时有效地计算商和余数。因此,j <= squareRoot
的限制是在1)无需昂贵的sqrt()
计算的情况下实现的; 2)对于大型number
是准确的,与sqrt()
的使用不同。
使用匹配的说明符。 u
,而不是无符号类型的d
。
// printf("Number %ld is a prime!\n", i);
printf("Number %lu is a prime!\n", i);
在这里使用全局i
是一种较弱的编码风格。建议重新编码并通过函数传递。
要获得更大的改进,请查看Sieve of Eratosthenes并保留以前找到的质数的列表,并测试这些质数而不是所有奇数。
高级测试是一门深入的主题,其中包含许多更高级的想法。