我的问题是关于试验部门的条件测试。关于采用什么条件测试似乎存在争议。让我们从RosettaCode查看代码。
int is_prime(unsigned int n)
{
unsigned int p;
if (!(n & 1) || n < 2 ) return n == 2;
/* comparing p*p <= n can overflow */
for (p = 3; p <= n/p; p += 2)
if (!(n % p)) return 0;
return 1;
}
轮子分解或使用预定的素数列表不会改变我的问题的本质。
有三种情况我可以考虑进行条件测试:
案例1:适用于所有n但它必须在每次迭代时进行额外的除法(编辑:实际上它不需要额外的除法但它仍然较慢。我不知道为什么。请参阅汇编输出下文)即可。我发现它的速度是情况2的两倍,因为大的n值是素数(在我的Sandy Bridge系统上)。
案例2:明显快于案例1,但它存在一个问题,即大n会溢出并进入不定式循环。它可以处理的最大值是
(sqrt(n) + c)^2 = INT_MAX //solve
n = INT_MAX -2*c*sqrt(INT_MAX) + c^2
//INT_MAX = 2^32 -> n = 2^32 - c*s^17 + c^2; in our case c = 2
例如对于uint64_t,情况2将进入x = -1L-58(x ^ 64-59)的无限循环,这是一个素数。
情况3:每次迭代都不需要进行除法或乘法运算,并且不像情况2那样溢出。它也比情况2快一些。唯一的问题是sqrt(n) is accurate enough。
有人可以向我解释为什么案例2比案例1快得多吗?案例1并没有像我那样使用额外的除法,但尽管它仍然慢得多。
以下是素数2 ^ 56-5的时间;
case 1 9.0s
case 2 4.6s
case 3 4.5s
以下是我用来测试http://coliru.stacked-crooked.com/a/69497863a97d8953的代码。我还在这个问题的最后添加了这些函数。
以下是用于案例1和案例2的GCC 4.8和-O3的汇编输出。它们都只有一个除法。情况2也有乘法,所以我的第一个猜测是情况2会慢一些,但它在GCC和MSVC上的速度大约是两倍。我不知道为什么。
案例1:
.L5:
testl %edx, %edx
je .L8
.L4:
addl $2, %ecx
xorl %edx, %edx
movl %edi, %eax
divl %ecx
cmpl %ecx, %eax
jae .L5
案例2:
.L20:
xorl %edx, %edx
movl %edi, %eax
divl %ecx
testl %edx, %edx
je .L23
.L19:
addl $2, %ecx
movl %ecx, %eax
imull %ecx, %eax
cmpl %eax, %edi
jae .L20
以下是我用来测试时间的函数:
int is_prime(uint64_t n)
{
uint64_t p;
if (!(n & 1) || n < 2 ) return n == 2;
/* comparing p*p <= n can overflow */
for (p = 3; p <= n/p; p += 2)
if (!(n % p)) return 0;
return 1;
}
int is_prime2(uint64_t n)
{
uint64_t p;
if (!(n & 1) || n < 2 ) return n == 2;
/* comparing p*p <= n can overflow */
for (p = 3; p*p <= n; p += 2)
if (!(n % p)) return 0;
return 1;
}
int is_prime3(uint64_t n)
{
uint64_t p;
if (!(n & 1) || n < 2 ) return n == 2;
/* comparing p*p <= n can overflow */
uint32_t cut = sqrt(n);
for (p = 3; p <= cut; p += 2)
if (!(n % p)) return 0;
return 1;
}
在赏金后添加了内容。
Aean发现在案例1中保存商和余数与案例2一样快(或稍快)。让我们称之为案例4.以下代码的速度是案例1的两倍。
int is_prime4(uint64_t n)
{
uint64_t p, q, r;
if (!(n & 1) || n < 2 ) return n == 2;
for (p = 3, q=n/p, r=n%p; p <= q; p += 2, q = n/p, r=n%p)
if (!r) return 0;
return 1;
}
我不确定为什么这会有所帮助。在任何情况下,都不需要再使用案例2。对于案例3,硬件或软件中sqrt
函数的大多数版本都可以获得完美的正方形,因此一般情况下使用它是安全的。案例3是唯一适用于OpenMP的案例。
答案 0 :(得分:4)
UPD:这显然是编译器优化问题。虽然MinGW在循环体中只使用了一条div
指令,但Linux和MSVC上的GCC都无法重用上一次迭代中的商。
我认为我们能做的最好是明确定义quo
和rem
并在同一基本指令块中计算它们,以显示我们想要商和余数的编译器。
int is_prime(uint64_t n)
{
uint64_t p = 3, quo, rem;
if (!(n & 1) || n < 2) return n == 2;
quo = n / p;
for (; p <= quo; p += 2){
quo = n / p; rem = n % p;
if (!(rem)) return 0;
}
return 1;
}
我在MinGW-w64编译器上尝试了来自http://coliru.stacked-crooked.com/a/69497863a97d8953的代码,case 1
比case 2
更快。
所以我猜测您正在编译针对32位架构并使用uint64_t
类型。您的程序集显示它不使用任何64位寄存器。
如果我做对了,那就有原因。
在32位体系结构中,64位数字表示在两个32位寄存器中,您的编译器将执行所有串联工作。进行64位加法,减法和乘法很简单。但模数和除法是通过一个小函数调用完成的,在GCC中命名为___umoddi3
和___udivdi3
,在MSVC中命名为aullrem
和aulldiv
。
实际上,___umoddi3
中每次迭代需要一个___udivdi3
和一个case 1
,___udivdi3
需要一个case 2
和一个64位乘法串联。这就是为什么case 1
在测试中看起来比case 2
慢两倍的原因。
case 1
中你真正得到了什么:
L5:
addl $2, %esi
adcl $0, %edi
movl %esi, 8(%esp)
movl %edi, 12(%esp)
movl %ebx, (%esp)
movl %ebp, 4(%esp)
call ___udivdi3 // A call for div
cmpl %edi, %edx
ja L6
jae L21
L6:
movl %esi, 8(%esp)
movl %edi, 12(%esp)
movl %ebx, (%esp)
movl %ebp, 4(%esp)
call ___umoddi3 // A call for modulo.
orl %eax, %edx
jne L5
case 2
中你真正得到了什么:
L26:
addl $2, %esi
adcl $0, %edi
movl %esi, %eax
movl %edi, %ecx
imull %esi, %ecx
mull %esi
addl %ecx, %ecx
addl %ecx, %edx
cmpl %edx, %ebx
ja L27
jae L41
L27:
movl %esi, 8(%esp)
movl %edi, 12(%esp)
movl %ebp, (%esp)
movl %ebx, 4(%esp)
call ___umoddi3 // Just one call for modulo
orl %eax, %edx
jne L26
MSVC未能重复使用div
的结果。优化由return
打破。
试试这些代码:
__declspec(noinline) int is_prime_A(unsigned int n)
{
unsigned int p;
int ret = -1;
if (!(n & 1) || n < 2) return n == 2;
/* comparing p*p <= n can overflow */
p = 1;
do {
p += 2;
if (p >= n / p) ret = 1; /* Let's return latter outside the loop. */
if (!(n % p)) ret = 0;
} while (ret < 0);
return ret;
}
__declspec(noinline) int is_prime_B(unsigned int n)
{
unsigned int p;
if (!(n & 1) || n < 2) return n == 2;
/* comparing p*p <= n can overflow */
p = 1;
do {
p += 2;
if (p > n / p) return 1; /* The common routine. */
if (!(n % p)) return 0;
} while (1);
}
对于Windows,is_prime_B
将比MSVC / ICC上的is_prime_A
慢两倍。
答案 1 :(得分:2)
sqrt(n)
只要你的sqrt
单调增加就足够准确,它就会得到完美的正方形,并且每个unsigned int
都可以完全表示为double
。在我所知道的每个平台上都有这三种情况。
你可以通过实现一个函数unsigned int sqrti(unsigned int n)
来解决这些问题(如果你认为它们是问题),函数unsigned int
使用Newton的方法返回{{1}}的平方根的底限。 (如果你以前从未做过这个,这是一个有趣的练习!)
答案 2 :(得分:2)
回答这篇文章的一小部分。
案例2修复以处理溢出。
#include <limits.h>
int is_prime(unsigned n) {
unsigned p;
if (!(n & 1) || n < 2)
return n == 2;
#define UINT_MAX_SQRT (UINT_MAX >> (sizeof(unsigned)*CHAR_BIT/2))
unsigned limit = n;
if (n >= UINT_MAX_SQRT * UINT_MAX_SQRT)
limit = UINT_MAX_SQRT * UINT_MAX_SQRT - 1;
for (p = 3; p * p < limit; p += 2)
if (!(n % p))
return 0;
if (n != limit)
if (!(n % p))
return 0;
return 1;
}
如果sizeof(unsigned)
和CHAR_BIT
都是奇数,则限额计算失败 - 这种情况很少见。
答案 3 :(得分:1)
关于你的第一个问题:为什么(2)比(1)更快? 嗯,这取决于编译器,也许 然而,一般来说,人们可以预期分割比乘法更昂贵。
关于你的第二个问题:sqrt()是一个准确的函数吗?
一般来说,这是准确的
可能给你带来问题的唯一情况是sqrt(n)
是一个整数
例如,如果您的系统中有n == 9
和sqrt(n) == 2.9999999999999
,那么您就遇到了麻烦,因为整数部分是2,但确切的值是3。
然而,这种罕见的情况很容易通过添加一个不那么小的双常数来处理,比如说
因此,你可以写:
double stop = sqrt(n) + 0.1;
for (unsigned int d = 2; d <= stop; d += 2)
if (n % d == 0)
break; /* not prime!! */
添加的术语0.1可以为您的算法添加一次迭代,这根本不是一个大问题。
最后,您的算法的明显选择是(3),即sqrt()
方法,因为没有任何计算(乘法或除法),并且值stop
仅计算一旦。
您可以获得的另一项改进如下:
p >= 5
的格式为6n - 1
或6n + 1
。 因此,您可以将变量d
的增量替换为2,4,2,4等。