Performance 这个素数相关谓词的瓶颈是什么?

Performance 这个素数相关谓词的瓶颈是什么?,performance,prolog,primes,Performance,Prolog,Primes,这里是:我试图计算所有低于两百万的素数之和,但是我的程序非常慢。我确实知道算法本身非常糟糕,是一种蛮力的算法,但在我看来,它似乎比应该的慢多了。 在这里,我将搜索限制为20000,这样结果就不会等待太久。 我不认为这个谓词很难理解,但我还是要解释一下:我计算20000以下所有素数的列表,然后求和。求和部分很好,素数部分很慢 problem_010(R) :- p010(3, [], Primes), sumlist([2|Primes], R). p010(20001, Prim

这里是:我试图计算所有低于两百万的素数之和,但是我的程序非常慢。我确实知道算法本身非常糟糕,是一种蛮力的算法,但在我看来,它似乎比应该的慢多了。
在这里,我将搜索限制为20000,这样结果就不会等待太久。
我不认为这个谓词很难理解,但我还是要解释一下:我计算20000以下所有素数的列表,然后求和。求和部分很好,素数部分很慢

problem_010(R) :-
    p010(3, [], Primes),
    sumlist([2|Primes], R).
p010(20001, Primes, Primes) :- !.
p010(Current, Primes, Result) :-
    (
        prime(Current, Primes)
    ->  append([Primes, [Current]], NewPrimes)
    ;   NewPrimes = Primes
    ),
    NewCurrent is Current + 2,
    p010(NewCurrent, NewPrimes, Result).
prime(_, []) :- !.
prime(N, [Prime|_Primes]) :- 0 is N mod Prime, !, fail.
prime(ToTest, [_|Primes]) :- prime(ToTest, Primes).
我想了解一下它为什么这么慢。它是愚蠢的暴力算法的一个很好的实现,还是有什么原因使Prolog失败


编辑:我已经发现了一些东西,通过添加新的素数而不是让它们出现在列表的开头,我的素数在开始时出现的频率更高,因此速度快了约3倍。不过仍然需要一些见解:)

首先,Prolog在这里并没有失败

有非常聪明的方法来生成素数。但作为一个廉价的开始,简单地以相反的顺序累积素数!(7.9s->2.6s)以这种方式,较小的测试更快。然后,考虑只对素数进行测试,直到141。较大的素数不能成为一个因素

然后,您可以添加3、5、7,而不是只通过不可被2整除的数字


有人在写关于这个“问题”的论文。例如,请看,尽管“真正的”算法实际上是什么有点诡辩,但在22世纪前,当最新版本的算盘被称为时。

首先,使用
append/3
在列表末尾追加是相当慢的。如果必须,则使用差异列表。(就我个人而言,我尽量避免
append/3

其次,在检查素数时,
prime/2
总是在整个列表中迭代。这是不必要的缓慢。您可以改为只检查id,您可以找到一个整数因子,其最大值为您要检查的数字的平方根

problem_010(R) :-
    p010(3, 2, R).
p010(2000001, Primes, Primes) :- !.
p010(Current, In, Result) :-
    ( prime(Current) -> Out is In+Current ; Out=In ),
    NewCurrent is Current + 2,
    p010(NewCurrent, Out, Result).

prime(2).
prime(3).
prime(X) :-
    integer(X),
    X > 3,
    X mod 2 =\= 0,
    \+is_composite(X, 3).   % was: has_factor(X, 3)

is_composite(X, F) :-       % was: has_factor(X, F) 
    X mod F =:= 0, !.
is_composite(X, F) :- 
    F * F < X,
    F2 is F + 2,
    is_composite(X, F2).
下面是更快的代码:

problem_010(R) :-
    Max = 2000001,
    functor(Bools, [], Max),
    Sqrt is integer(floor(sqrt(Max))),
    remove_multiples(2, Sqrt, Max, Bools),
    compute_sum(2, Max, 0, R, Bools).

% up to square root of Max, remove multiples by setting bool to 0
remove_multiples(I, Sqrt, _, _) :- I > Sqrt, !.
remove_multiples(I, Sqrt, Max, Bools) :-
    arg(I, Bools, B),
    (
        B == 0
    ->
        true % already removed: do nothing
    ;
        J is 2*I, % start at next multiple of I
        remove(J, I, Max, Bools)
    ),
    I1 is I+1,
    remove_multiples(I1, Sqrt, Max, Bools).

remove(I, _, Max, _) :- I > Max, !.
remove(I, Add, Max, Bools) :-
     arg(I, Bools, 0), % remove multiple by setting bool to 0
     J is I+Add,
     remove(J, Add, Max, Bools).

% sum up places that are not zero
compute_sum(Max, Max, R, R, _) :- !.
compute_sum(I, Max, RI, R, Bools) :-
    arg(I, Bools, B),
    (B == 0 -> RO = RI ;  RO is RI + I ),
    I1 is I+1,
    compute_sum(I1, Max, RO, R, Bools).
这比我上面给出的代码运行快一个数量级:

?- problem_010(R).
R = 142913828922
Yes (0.82s cpu)

例如,考虑使用一种筛选方法(“埃拉托什尼的筛选”):首先使用例如numlist/3创建一个列表[2,3,4,5,6,….N]。列表中的第一个数字是素数,保留它。从列表的其余部分中删除其倍数。剩余列表中的下一个数字也是素数。再次消除其倍数。等等列表将迅速缩小,最后只剩下素数。

好的,在编辑之前,问题只是算法(imho)。 正如您所注意到的,首先检查数字是否除以较小的素数更有效;在有限集合中,被3整除的数字比被32147整除的数字多

另一个算法改进是当素数大于数字的平方根时停止检查

现在,在您的更改之后,确实存在一些序言问题: 您可以使用append/3。append/3相当慢,因为必须遍历整个列表才能将元素放在末尾。 相反,您应该使用差异列表,这使得将元素放置在尾部的速度非常快

现在,区别列表是什么?不是创建一个普通列表[1,2,3],而是创建一个[1,2,3|T]。请注意,我们没有对尾部进行标定。然后,如果我们想在列表末尾添加一个(或多个)元素,我们可以简单地说T=[4 | NT]。太棒了

以下解决方案(按相反顺序累积素数,当素数>sqrt(N)时停止,要追加的差异列表)对于20k素数需要0.063秒,对于2m素数需要17秒,而原始代码对于20k和append/3版本1.3sec需要3.7秒

problem_010(R) :-
    p010(3, Primes, Primes),
    sumlist([2|Primes], R).
p010(2000001, _Primes,[]) :- !.                                %checking for primes till 2mil
p010(Current, Primes,PrimesTail) :-
    R is sqrt(Current),
    (
        prime(R,Current, Primes)
    ->  PrimesTail = [Current|NewPrimesTail]
    ;   NewPrimesTail = PrimesTail
    ),
    NewCurrent is Current + 2,
    p010(NewCurrent, Primes,NewPrimesTail).
prime(_,_, Tail) :- var(Tail),!.
prime(R,_N, [Prime|_Primes]):-
    Prime>R.
prime(_R,N, [Prime|_Primes]) :-0 is N mod Prime, !, fail.
prime(R,ToTest, [_|Primes]) :- prime(R,ToTest, Primes).
另外,考虑在生成数字时添加数字,以避免由于sumlist/2而产生额外的o(n)

最后,您始终可以实现以多项式时间(XD)运行的AKS算法。

@false这很有趣。事实上,它返回了142913828922,尽管我发誓它第一次返回了21171191:it’it’我查错了,我收回了我的评论:你使用的是一个更大的数字!不是20001而是2000001@false是的,我刚刚意识到这是一个很好的例子!我知道差异列表,但以前从未真正使用过,谢谢你的建议!我不想再次破坏这一点,但这种差异列表通常会导致非常难以维护的程序,比您可以想象的任何命令式程序都糟糕。在运行中计算总和,并仅使用该列表来累积低于sqrt(2000001)的素数,似乎要干净得多。无论如何,一个比+2更好的步进器将是最有趣的改进。当我完成这个蛮力的事情时,我将实施它,看看它是如何进行的:)谢谢你的建议!您应该只将数字标记为复合数字,而不是删除它们,否则您将无法正确计数,并将被迫单独测试每个数字,或将它们与生成的倍数进行比较——这比仅在给定偏移量处标记数字更糟糕。当然,在这里,使用复合词作为数组替代物是一个巨大的性能提升。非常好的prime/1和有\u factor/2谓词!谢谢,谢谢,但正如我说的,它们不是我的。我通过谷歌搜索erastothenes的筛子找到了它们。但是,我在我的答案中添加了另一个版本,它不使用这些谓词,并且通过避免列表更快。第二个版本的主要思想是使用术语而不是列表,因此,我可以通过使用
arg/3
来更快地删除倍数,而不是迭代列表来查找它们。您的示例非常有趣。谢谢你发布它们。(我把答案留给了thanosQR,因为它更多的是关于主题的,但我真的很感激:)如果可以的话,我会+2!为您修复了一个错误。:)您可以从
I*I
开始删除,而不仅仅是
2*I
,这将给您带来另一个加速
problem_010(R) :-
    p010(3, Primes, Primes),
    sumlist([2|Primes], R).
p010(2000001, _Primes,[]) :- !.                                %checking for primes till 2mil
p010(Current, Primes,PrimesTail) :-
    R is sqrt(Current),
    (
        prime(R,Current, Primes)
    ->  PrimesTail = [Current|NewPrimesTail]
    ;   NewPrimesTail = PrimesTail
    ),
    NewCurrent is Current + 2,
    p010(NewCurrent, Primes,NewPrimesTail).
prime(_,_, Tail) :- var(Tail),!.
prime(R,_N, [Prime|_Primes]):-
    Prime>R.
prime(_R,N, [Prime|_Primes]) :-0 is N mod Prime, !, fail.
prime(R,ToTest, [_|Primes]) :- prime(R,ToTest, Primes).