对于使用列表 (@pad) 的一般试验除法算法和使用埃拉托色尼筛 (SoE) (@John Palmer 和 @Jon Harrop) 选择用于筛选数据结构的数组,已经有几个很好的答案。但是,@pad 的列表算法并不是特别快,并且对于更大的筛选范围会“炸毁”,而@John Palmer 的数组解决方案稍微复杂一些,使用的内存比必要的多,并且使用外部可变状态,因此与程序是用 C# 等命令式语言编写的。
EDIT_ADD: 我编辑了下面的代码(带有行注释的旧代码)修改序列表达式以避免一些函数调用,以反映更多的“迭代器样式”,虽然它节省了 20% 的速度,但它仍然没有接近真正的 C# 迭代器,其速度与“滚动您自己的枚举器”最终 F# 代码的速度大致相同。我已经相应地修改了下面的时间信息。 END_EDIT
以下真正的 SoE 程序仅使用 64 KB 的内存来筛选多达一百万的素数(由于仅考虑奇数并使用压缩位 BitArray),并且仍然几乎与 @John Palmer 的程序一样快,大约需要 40 毫秒来筛选i7 2700K (3.5 GHz) 上一百万,只有几行代码:
open System.Collections
let primesSoE top_number=
let BFLMT = int((top_number-3u)/2u) in let buf = BitArray(BFLMT+1,true)
let SQRTLMT = (int(sqrt (double top_number))-3)/2
let rec cullp i p = if i <= BFLMT then (buf.[i] <- false; cullp (i+p) p)
for i = 0 to SQRTLMT do if buf.[i] then let p = i+i+3 in cullp (p*(i+1)+i) p
seq { for i = -1 to BFLMT do if i<0 then yield 2u
elif buf.[i] then yield uint32(3+i+i) }
// seq { yield 2u; yield! seq { 0..BFLMT } |> Seq.filter (fun i->buf.[i])
// |> Seq.map (fun i->uint32 (i+i+3)) }
primesSOE 1000000u |> Seq.length;;
由于序列运行时库的低效率以及在每个函数调用大约 28 个时钟周期并以大约 16 个函数调用返回的成本,几乎所有经过的时间都花费在最后两行枚举找到的素数上每次迭代。通过滚动我们自己的迭代器,这可以减少到每次迭代只有几个函数调用,但代码并不那么简洁;请注意,在以下代码中,除了筛选数组的内容和使用对象表达式的迭代器实现所必需的引用变量之外,没有暴露可变状态:
open System
open System.Collections
open System.Collections.Generic
let primesSoE top_number=
let BFLMT = int((top_number-3u)/2u) in let buf = BitArray(BFLMT+1,true)
let SQRTLMT = (int(sqrt (double top_number))-3)/2
let rec cullp i p = if i <= BFLMT then (buf.[i] <- false; cullp (i+p) p)
for i = 0 to SQRTLMT do if buf.[i] then let p = i+i+3 in cullp (p*(i+1)+i) p
let nmrtr() =
let i = ref -2
let rec nxti() = i:=!i+1;if !i<=BFLMT && not buf.[!i] then nxti() else !i<=BFLMT
let inline curr() = if !i<0 then (if !i= -1 then 2u else failwith "Enumeration not started!!!")
else let v = uint32 !i in v+v+3u
{ new IEnumerator<_> with
member this.Current = curr()
interface IEnumerator with
member this.Current = box (curr())
member this.MoveNext() = if !i< -1 then i:=!i+1;true else nxti()
member this.Reset() = failwith "IEnumerator.Reset() not implemented!!!"
interface IDisposable with
member this.Dispose() = () }
{ new IEnumerable<_> with
member this.GetEnumerator() = nmrtr()
interface IEnumerable with
member this.GetEnumerator() = nmrtr() :> IEnumerator }
primesSOE 1000000u |> Seq.length;;
上面的代码在同一台机器上将素数筛选到一百万需要大约 8.5 毫秒,因为每次迭代的函数调用数量从大约 16 个大大减少到大约 3 个。这与以相同风格编写的 C# 代码的速度大致相同. 太糟糕了,我在第一个示例中使用的 F# 的迭代器样式不会像 C# 迭代器那样自动生成 IEnumerable 样板代码,但我想这就是序列的意图——只是它们在加速性能方面效率太低了由于被实现为序列计算表达式。
现在,只有不到一半的时间用于枚举主要结果,以便更好地利用 CPU 时间。