【问题标题】:.NET Optimization of F# Sieve of Eratosthenes.NET 优化 Eratosthenes 的 F# Sieve
【发布时间】:2018-08-10 04:12:51
【问题描述】:

我正在摆弄 F# 并使用 FSI REPL,我注意到在我的初学者的幼稚 Eratosthenes 实现的两个略有不同的实现之间存在巨大的效率差异。第一个带有额外的 if:

let rec sieve max current pList =
    match current with
    | 2 -> sieve max (current + 1) (current::pList)
    | 3 -> sieve max (current + 2) (current::pList)
    | n when n < max ->
        if (n % 5 = 0) || (n % 3 = 0) then
            sieve max (current + 2) (current::pList)
        else if (pList |> (List.map (fun n -> current % n = 0)) |> List.contains true) then
            sieve max (current + 2) (pList)
        else
            sieve max (current + 2) (current::pList)
    | n when n >= max
        -> pList
    | _
        ->  printfn "Error: list length: %A, current: %A" (List.length pList) current
            [-1]

第二个没有:

let rec sieve max current pList =
    match current with
    | 2 -> sieve max (current + 1) (current::pList)
    | 3 -> sieve max (current + 2) (current::pList)
    | n when n < max ->
        if (pList |> (List.map (fun n -> current % n = 0)) |> List.contains true) then
            sieve max (current + 2) (pList)
        else
            sieve max (current + 2) (current::pList)
    | n when n >= max
        -> pList
    | _
        ->  printfn "Error: list length: %A, current: %A" (List.length pList) current
            [-1]

具有额外if 分支的分支实际上更慢,尽管它看起来应该会更快。 然后,我使用以下方法对 REPL 中的两个实现进行计时:

#time sieve 200000 2 [] #time

并在我的机器上看到,带有额外 if 语句的实现大约需要 2 分钟,而没有的实现每次运行大约需要 1 分钟。 这怎么可能?通过添加处理 3 或 5 的倍数的 if 语句,它实际上比仅映射整个列表然后查找素数列表中是否有任何数字的除数要慢。 如何? 仅仅是 F# 对处理列表进行了优化吗?

【问题讨论】:

    标签: .net optimization f# sieve-of-eratosthenes f#-interactive


    【解决方案1】:

    当然,列表不一定有效。我曾经创建了一个函数来创建一个布尔数组,其中每个素数都为真,每个非素数都为假:

    let sieveArray limit =
        let a = Array.create limit true
        let rec setArray l h s x =
            if l <= h then
                a.[l] <- x
                setArray (l + s) h s x
        a.[0] <- false; a.[1] <- false
        for i = 0 to a.Length - 1 do
            if a.[i]
            then setArray (i + i) (a.Length - 1) i false
        a
    

    要获得实际素数的列表,您只需映射索引,过滤结果数组:

    sieveArray limit
    |> Seq.mapi (fun i x -> i, x)
    |> Seq.filter snd
    |> Seq.map fst
    |> Seq.toList
    

    【讨论】:

      【解决方案2】:

      第一个筛子中的额外 if 可能是一种快捷方式,实际上会稍微改变行为。它实际上添加了它,而不是删除除以 3 和 5 的所有内容。比较一下输出就很容易看出:

      1st sieve: [19; 17; 15; 13; 11; 9; 7; 5; 3; 2]
      2st sieve: [19; 17; 13; 11; 7; 5; 3; 2]
      

      我假设你想要的是这样的:

      if (n % 5 = 0) || (n % 3 = 0) then
          sieve max (current + 2) (pList)
      

      但是,在这种情况下,它不包括 5(显然)。所以正确的代码是

      if (n <> 5 && n % 5 = 0) || (n <> 3 && n % 3 = 0) then
          sieve max (current + 2) (pList)
      

      检查上面代码的性能 - 应该没问题。

      【讨论】:

      • 啊。所以真正的错误是我缺乏代码修改:)
      【解决方案3】:

      额外的if 执行额外的计算,但不会中断执行流程,它愉快地继续到第二个if。所以实际上,你只是在你的函数中添加了一些无用的计算。难怪现在需要更长的时间!

      你一定在想这样的事情:

      if (a)
          return b;
      if (x)
          return y;
      else 
          return z;
      

      嗯,这在 F# 中的工作方式与在 C# 或 Java 或任何您所想的方式中的工作方式不同。 F# 没有“提前返回”。没有“陈述”,一切都是表达,一切都有结果。

      添加这种无用的计算实际上会产生警告。如果您注意警告,您应该注意到“此值正在被丢弃”或类似的内容。编译器试图通过指向一个无用的函数调用来帮助您。

      要解决此问题,请将第二个 if 替换为 elif

      if (n % 5 = 0) || (n % 3 = 0) then
          sieve max (current + 2) (current::pList)
      elif (pList |> (List.map (fun n -> current % n = 0)) |> List.contains true) then
          sieve max (current + 2) (pList)
      else
          sieve max (current + 2) (current::pList)
      

      这将使第二个分支仅在第一个分支失败时执行。

      附:想想看,没有elseif 甚至不应该编译,因为它的结果类型无法确定。我不确定那里发生了什么。

      P.P.S. List.map f |&gt; List.contains true 更好地表示为 List.exists f。更短性能更高。

      【讨论】:

      • 其实这是一个复制粘贴错误。我更正了显示两个版本。
      • 哦,好吧... :-)
      猜你喜欢
      • 2012-02-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-03
      • 1970-01-01
      • 1970-01-01
      • 2016-04-05
      • 1970-01-01
      相关资源
      最近更新 更多