【问题标题】:Why is iterating through an array faster than Seq.find为什么迭代数组比 Seq.find 更快
【发布时间】:2016-02-11 18:39:38
【问题描述】:

我有一个数组sums,它给出了函数f 的所有可能总和。此函数接受整数(例如 1 到 200 之间,但同样适用于 1 和 10000)并将它们转换为双精度数。我想将sums 存储为一个数组,因为我还没有弄清楚如何在没有循环的情况下执行我需要的算法。

这是我如何生成 sums 的代码:

let f n k = exp (double(k)/double(n)) - 1.0


let n = 200
let maxLimit = int(Math.Round(float(n)*1.5))

let FunctionValues = [|1..maxLimit|] |> Array.map (fun k -> f n k)

let sums = FunctionValues |> Array.map (fun i -> Array.map (fun j -> j + i) FunctionValues) |> Array.concat |> Array.sort

我发现数组sums 的某些元素,我想找到一些整数,当输入到函数f 中然后添加将等于sums 中的值。我可以将整数存储在sums 中,但我发现这会破坏我的记忆。

现在我有两种算法。算法 1 使用一个简单的循环和一个可变的 int 来存储我关心的值。它应该不是很有效,因为在找到所有可能的整数时没有 break 语句。我尝试实现更实用的算法 2,但我发现它更慢(慢约 10% 或 4200 毫秒对 4600 毫秒,n = 10000),尽管Seq 很懒惰。这是为什么呢?

算法一:

let mutable a = 0
let mutable b = 0
let mutable c = 0
let mutable d = 0
for i in 1..maxLimit do
    for j in i..maxLimit do
        if sums.[bestI] = f n i + f n j then
            a <- i
            b <- j
        if sums.[bestMid] = f n i + f n j then
            c <- i
            d <- j

算法2:

let findNM x = 
    let seq = {1..maxLimit} |> Seq.map (fun k -> (f n k, k))
    let get2nd3rd (a, b, c) = (b, c)
    seq |> Seq.map (fun (i, n) -> Seq.map (fun (j, m) -> (j + i, n, m) ) seq) 
        |> Seq.concat |> Seq.find (fun (i, n, m) -> i = x)
        |>  get2nd3rd

let digitsBestI = findNM sums.[bestI]
let digitsBestMid = findNM sums.[bestMid]

let a = fst digitsBestI
let b = snd digitsBestI
let c = fst digitsBestMid
let d = snd digitsBestMid

编辑:注意数组sums 的长度是maxLimit*maxLimit 而不是长度n。 bestI 和 bestMid 然后是 0 和 maxLimit*maxLimit 之间的索引。就本问题而言,它们可以是该范围内的任何数字。它们的具体值并不特别相关。

【问题讨论】:

  • 算法实际上在做什么? bestI 和 bestMid 未定义
  • 它们只是我想要的 sums 数组的索引。不是特别相关。它们在这里的重要性可能是随机的。
  • 我在关于sums 的大小以及bestI 和bestMid 是什么的问题中添加了注释。

标签: arrays algorithm f#


【解决方案1】:

为了分析它,我稍微扩展了 OP 代码

open System

let f n k   = exp (double(k)/double(n)) - 1.0

let outer   = 200
let n       = 200
let maxLimit= int(Math.Round(float(n)*1.5))

let FunctionValues = [|1..maxLimit|] |> Array.map (fun k -> f n k)

let random = System.Random 19740531

let sums = FunctionValues |> Array.map (fun i -> Array.map (fun j -> j + i) FunctionValues) |> Array.concat |> Array.sort

let bests = 
  [| for i in [1..outer] -> (random.Next (n, maxLimit*maxLimit), random.Next (n, maxLimit*maxLimit))|]

let stopWatch = 
  let sw = System.Diagnostics.Stopwatch ()
  sw.Start ()
  sw

let timeIt (name : string) (a : int*int -> 'T) : unit = 
  let t = stopWatch.ElapsedMilliseconds
  let v = a (bests.[0])
  for i = 1 to (outer - 1) do
    a bests.[i] |> ignore
  let d = stopWatch.ElapsedMilliseconds - t
  printfn "%s, elapsed %d ms, result %A" name d v

let algo1 (bestI, bestMid) =
  let mutable a = 0
  let mutable b = 0
  let mutable c = 0
  let mutable d = 0
  for i in 1..maxLimit do
    for j in i..maxLimit do
      if sums.[bestI] = f n i + f n j then
        a <- i
        b <- j
      if sums.[bestMid] = f n i + f n j then
        c <- i
        d <- j

  a,b,c,d

let algo2 (bestI, bestMid) =
  let findNM x = 
    let seq = {1..maxLimit} |> Seq.map (fun k -> (f n k, k))
    let get2nd3rd (a, b, c) = (b, c)
    seq |> Seq.map (fun (i, n) -> Seq.map (fun (j, m) -> (j + i, n, m) ) seq) 
        |> Seq.concat |> Seq.find (fun (i, n, m) -> i = x)
        |> get2nd3rd

  let digitsBestI = findNM sums.[bestI]
  let digitsBestMid = findNM sums.[bestMid]

  let a = fst digitsBestI
  let b = snd digitsBestI
  let c = fst digitsBestMid
  let d = snd digitsBestMid

  a,b,c,d

let algo3 (bestI, bestMid) =
  let rec find best i j = 
    if best = f n i + f n j then i, j
    elif i = maxLimit && j = maxLimit then 0, 0
    elif j = maxLimit then find best (i + 1) 1
    else find best i (j + 1)
  let a, b = find sums.[bestI] 1 1
  let c, d = find sums.[bestMid] 1 1
  a, b, c, d

let algo4 (bestI, bestMid) =
  let rec findI bestI mid i j = 
    if bestI = f n i + f n j then 
      let x, y = mid
      i, j, x, y
    elif i = maxLimit && j = maxLimit then 0, 0, 0, 0
    elif j = maxLimit then findI bestI mid (i + 1) 1
    else findI bestI mid i (j + 1)

  let rec findMid ii bestMid i j = 
    if bestMid = f n i + f n j then 
      let x, y = ii
      x, y, i, j
    elif i = maxLimit && j = maxLimit then 0, 0, 0, 0
    elif j = maxLimit then findMid ii bestMid (i + 1) 1
    else findMid ii bestMid i (j + 1)

  let rec find bestI bestMid i j = 
    if bestI = f n i + f n j then findMid (i, j) bestMid i j
    elif bestMid = f n i + f n j then findI bestI (i, j) i j
    elif i = maxLimit && j = maxLimit then 0, 0, 0, 0
    elif j = maxLimit then find bestI bestMid (i + 1) 1
    else find bestI bestMid i (j + 1)

  find sums.[bestI] sums.[bestMid] 1 1

[<EntryPoint>]
let main argv =

  timeIt "algo1" algo1
  timeIt "algo2" algo2
  timeIt "algo3" algo3
  timeIt "algo4" algo4

  0

我机器上的测试结果:

algo1, elapsed 438 ms, result (162, 268, 13, 135)
algo2, elapsed 1012 ms, result (162, 268, 13, 135)
algo3, elapsed 348 ms, result (162, 268, 13, 135)
algo4, elapsed 322 ms, result (162, 268, 13, 135)

algo1 使用幼稚的for loop 实现。 algo2 使用依赖于Seq.find 的更精细的算法。我稍后描述algo3 和algo4。

OP 想知道为什么天真的 algo1 比基于懒惰的 Seq(本质上是 IEnumerable&lt;&gt;)的 algo2 表现更好,即使它做更多的工作。

答案是Seq 抽象引入了开销并阻止了有用的优化发生。

我通常求助于查看生成的 IL 代码以了解发生了什么(有很多好的 .NET 反编译器,例如 ILSpy)。

我们看algo1(反编译成C#)

// Program
public static Tuple<int, int, int, int> algo1(int bestI, int bestMid)
{
  int a = 0;
  int b = 0;
  int c = 0;
  int d = 0;
  int i = 1;
  int maxLimit = Program.maxLimit;
  if (maxLimit >= i)
  {
    do
    {
      int j = i;
      int maxLimit2 = Program.maxLimit;
      if (maxLimit2 >= j)
      {
        do
        {
          if (Program.sums[bestI] == Math.Exp((double)i / (double)200) - 1.0 + (Math.Exp((double)j / (double)200) - 1.0))
          {
            a = i;
            b = j;
          }
          if (Program.sums[bestMid] == Math.Exp((double)i / (double)200) - 1.0 + (Math.Exp((double)j / (double)200) - 1.0))
          {
            c = i;
            d = j;
          }
          j++;
        }
        while (j != maxLimit2 + 1);
      }
      i++;
    }
    while (i != maxLimit + 1);
  }
  return new Tuple<int, int, int, int>(a, b, c, d);
}

algo1 然后扩展为有效的while loop。此外,f 是内联的。 JITter 可以很容易地从中创建高效的机器代码。

当我们查看 algo2 时,拆开完整的结构对于这篇文章来说太多了,所以我专注于 findNM

internal static Tuple<int, int> findNM@48(double x)
{
  IEnumerable<Tuple<double, int>> seq = SeqModule.Map<int, Tuple<double, int>>(new Program.seq@49(), Operators.OperatorIntrinsics.RangeInt32(1, 1, Program.maxLimit));
  FSharpTypeFunc get2nd3rd = new Program.get2nd3rd@50-1();
  Tuple<double, int, int> tupledArg = SeqModule.Find<Tuple<double, int, int>>(new Program.findNM@52-1(x), SeqModule.Concat<IEnumerable<Tuple<double, int, int>>, Tuple<double, int, int>>(SeqModule.Map<Tuple<double, int>, IEnumerable<Tuple<double, int, int>>>(new Program.findNM@51-2(seq), seq)));
  FSharpFunc<Tuple<double, int, int>, Tuple<int, int>> fSharpFunc = (FSharpFunc<Tuple<double, int, int>, Tuple<int, int>>)((FSharpTypeFunc)((FSharpTypeFunc)get2nd3rd.Specialize<double>()).Specialize<int>()).Specialize<int>();
  return Program.get2nd3rd@50<double, int, int>(tupledArg);
}

我们看到它需要创建多个实现IEnumerable&lt;&gt; 的对象以及传递给更高阶函数(如Seq.find)的函数对象。虽然 JITter 原则上可以内联循环,但由于时间限制和内存原因,它很可能不会。这意味着对函数对象的每次调用都是一次虚拟调用,虚拟调用非常昂贵(提示:检查机器代码)。因为虚拟调用可能会做任何反过来妨碍优化的事情,例如使用 SIMD 指令。

OP 指出 F# 循环表达式缺少 break/continue 构造,这在编写高效 for loops 时很有用。然而,F# 确实支持它,因为如果您编写一个尾递归函数,F# 会将其展开为一个有效的循环,该循环使用break/continue 提前退出。

algo3 是使用尾递归实现algo2 的示例。反汇编后的代码是这样的:

internal static Tuple<int, int> find@66(double best, int i, int j)
{
  while (best != Math.Exp((double)i / (double)200) - 1.0 + (Math.Exp((double)j / (double)200) - 1.0))
  {
    if (i == Program.maxLimit && j == Program.maxLimit)
    {
      return new Tuple<int, int>(0, 0);
    }
    if (j == Program.maxLimit)
    {
      double arg_6F_0 = best;
      int arg_6D_0 = i + 1;
      j = 1;
      i = arg_6D_0;
      best = arg_6F_0;
    }
    else
    {
      double arg_7F_0 = best;
      int arg_7D_0 = i;
      j++;
      i = arg_7D_0;
      best = arg_7F_0;
    }
  }
  return new Tuple<int, int>(i, j);
}

这使我们能够编写惯用的函数式代码,同时获得非常好的性能,同时避免堆栈溢出。

在我意识到在 F# 中实现了多么好的尾递归之前,我尝试在 while 测试表达式中使用可变逻辑编写高效的 while 循环。为了人类,现在已经废除了代码。

algo4 是一个优化版本,它只对bestMid 和bestI 重复一次sums,这与algo1 非常相似,但algo4 会提前退出。

希望对你有帮助

【讨论】:

  • 嘿,这实际上很有帮助,尤其是关于 algo3 的部分。您犯的一个错误是最佳随机整数的范围应在maxLimit*maxLimit 之间,不小于n。进行更改并运行我得到的程序:algo1, elapsed 4436 ms, result (100, 297, 56, 106) algo2, elapsed 8035 ms, result (100, 297, 56, 106) algo3, elapsed 3166 ms, result (100, 297, 56, 106),这表明 algo2 运行最慢。
  • 这是有道理的。有时间我会更新代码。
  • 还请注意,当您迭代变量i 时,您实际上可以在变量i 上启动j,就像algo1 中的幼稚实现一样。所以,它应该是这样的:elif j = maxLimit then find best (i + 1) (i + 1)
猜你喜欢
  • 2012-01-31
  • 2022-11-22
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-17
  • 1970-01-01
  • 2012-05-16
相关资源
最近更新 更多