【问题标题】:Performance of .Net function calling (C# F#) VS C++.Net 函数调用 (C# F#) VS C++ 的性能
【发布时间】:2011-03-15 14:12:53
【问题描述】:

自从 F# 2.0 成为 VS2010 的一部分后,我对 F# 产生了兴趣。我想知道使用它有什么意义。我读了一点,我做了一个基准来衡量函数调用。 我用过阿克曼函数:)

C#

sealed class Program
{
    public static int ackermann(int m, int n)
    {
        if (m == 0)
            return n + 1;
        if (m > 0 && n == 0)
        {
            return ackermann(m - 1, 1);
        }
        if (m > 0 && n > 0)
        {
            return ackermann(m - 1, ackermann(m, n - 1));
        }
        return 0;
    }

    static void Main(string[] args)
    {

        Stopwatch stopWatch = new Stopwatch();

        stopWatch.Start();
        Console.WriteLine("C# ackermann(3,10) = " + Program.ackermann(3, 10));
        stopWatch.Stop();

        Console.WriteLine("Time required for execution: " + stopWatch.ElapsedMilliseconds + "ms");
        Console.ReadLine();
    }
}

C++

class Program{
public:
static inline int ackermann(int m, int n)
{
  if(m == 0)
       return n + 1;
  if (m > 0 && n == 0)
  {
      return ackermann(m - 1, 1);
  }
  if (m > 0 && n > 0)
  {
      return ackermann(m - 1, ackermann(m, n - 1));
  }
  return 0;
 }
};

int _tmain(int argc, _TCHAR* argv[])
{
 clock_t start, end;

  start = clock();
 std::cout << "CPP: ackermann(3,10) = " << Program::ackermann(3, 10) << std::endl;
 end = clock();
 std::cout << "Time required for execution: " << (end-start) << " ms." << "\n\n";
 int i;
 std::cin >> i;
 return 0;
}

F#

// Ackermann
let rec ackermann m n  =
  if m = 0 then n + 1
  elif m > 0 && n = 0 then ackermann (m - 1) 1
  elif m > 0 && n > 0 then ackermann (m - 1)  (ackermann m (n - 1))
  else 0

open System.Diagnostics;
let stopWatch = Stopwatch.StartNew()
let x = ackermann 3 10 
stopWatch.Stop();

printfn "F# ackermann(3,10) = %d"  x
printfn "Time required for execution: %f"  stopWatch.Elapsed.TotalMilliseconds

Java

public class Main 
{
 public static int ackermann(int m, int n)
 {
 if (m==0) 
   return n + 1;
if (m>0 && n==0)
{
 return ackermann(m - 1,1);
}
if (m>0 && n>0)
{
  return ackermann(m - 1,ackermann(m,n - 1));
 }
 return 0;
}

  public static void main(String[] args)
  { 
   System.out.println(Main.ackermann(3,10));
  }
}

然后
C# = 510 毫秒
c++ = 130 毫秒
F# = 185 毫秒
Java = Stackoverflow :)

如果我们想使用 .Net 并获得更快的执行速度,是不是 F# 的力量(除了少量代码)?我可以优化任何这些代码(尤其是 F#)吗?

更新。我摆脱了 Console.WriteLine 并在没有调试器的情况下运行 C# 代码:C# = 400ms

【问题讨论】:

  • 在运行基准测试之前执行一次您的方法。您的时间包括 JIT 中间语言所花费的时间。此外,在基准测试之外采用 Console.WriteLine() 等方法,因为它们非常慢。
  • "在运行基准测试之前执行一次你的方法。你的时间已经包括了 JIT 中间语言的时间。" 是吗?我添加了 let dd = ackermann 3 10 并给了我 7 毫秒的额外时间。对于 C# 没有改变 :) “另外,在基准测试之外采用 Console.WriteLine() 之类的方法,因为它们非常慢。”好主意,但没有加快速度
  • 是的,F# 和 C# 都是 JIT 编译的 - 运行该方法两次并使用第二个基准测试。原因是 JIT 编译器针对您的处理器的特定功能(CISC 计算优势)优化机器代码
  • 为什么不在基准测试中包含第一次执行?我们真的可以忽略 JIT 吗?这不是作弊吗?
  • 并非如此。您可以对应用程序进行 prejit,如果应用程序运行一个小时 - 第一个方法调用将根本不相关。

标签: c# .net c++ f# stopwatch


【解决方案1】:

我相信在这种情况下,C# 和 F# 的区别在于尾调用优化。

尾调用是指在函数中作为“最后一件事”完成的递归调用。在这种情况下,F#实际上并没有生成调用指令,而是将代码编译成一个循环(因为调用是“最后一件事”,我们可以重用当前堆栈帧,直接跳转到方法的开头) .

在您的ackermann 实现中,两个调用是尾调用,只有一个不是尾调用(结果作为参数传递给另一个ackermann 调用的那个)。这意味着 F# 版本实际上调用“调用”指令的频率更低(很多?)。

一般来说,性能配置文件与 C# 的性能配置文件大致相同。这里有很多与 F# 与 C# 性能相关的帖子:

【讨论】:

  • 我一直在用 C# 手工编写递归下降解析器,但由于缺乏尾调用优化,我经常后悔没有使用 F#。
  • ChaosPandion:您正在用 C# 编写解析器,而您后悔没有使用 F# 的唯一原因是它可能无法优化尾调用?真的吗?这是唯一的原因?
  • @lukas:VC++ 编译器(因此是优化器)比 C# 编译器成熟得多。即使是在 CLR 上运行的 C++/CLI 代码通常也比等效的 C# 执行得更好。实际上,F# 性能如此接近原生 C++ 的事实证明了 CLR 代码的潜在性能。
  • @lukas:我对 C++ 无能为力,因为我并不是真正的 C++ 专家(但它肯定不会进行尾调用优化)。
  • 其实有些C++编译器do tail-call optimization,其中有Visual C++。
【解决方案2】:

这是一种与函数调用相关的,因为它是减少函数调用的常用方法。

您可以通过记忆(缓存)使这种类型的递归函数运行得更快。您也可以在 C# 中执行此操作 (example。) 我有 18 毫秒。

open System.Collections.Generic

let memoize2 f =
    let cache = Dictionary<_, _>()
    fun x y ->
        if cache.ContainsKey (x, y) then 
            cache.[(x, y)]
        else 
            let res = f x y
            cache.[(x, y)] <- res
            res

// Ackermann
let rec ackermann =
    memoize2 (fun m n ->
        if m = 0 then n + 1
        elif m > 0 && n = 0 then ackermann (m - 1) 1
        elif m > 0 && n > 0 then ackermann (m - 1)  (ackermann m (n - 1))
        else 0
    )

【讨论】:

    【解决方案3】:

    与问题没有直接关系,但值得一提:试试这个版本

    let rec ackermann2 m n  =
      match m,n with
      | 0,0 -> 0
      | 0,n -> n+1
      | m,0 -> ackermann2 (m-1) 1
      | m,n -> ackermann2 (m-1) (ackermann2 m (n-1))
    

    在我的 PC(VS2010,F# 交互式)上计算 ackermann 3 12 时,它比您的版本快 50%。

    我无法完全解释为什么会有如此大的性能差异。我想这与 F# 编译器将匹配表达式转换为 switch 语句而不是一系列 if 语句有关。将 (m=0,n=0) 测试放在首位也可能有所帮助。

    【讨论】:

    • 模式匹配编译器应该重组测试以尽量减少冗余,并且原始代码执行了两次m&gt;0 测试。
    【解决方案4】:

    对于 F#,您可能想尝试使用 inline 关键字。

    此外,正如之前的海报所述,C# 和 F# 版本因 Console.WriteLine 语句而有所不同。

    【讨论】:

    • 函数不能内联和记录
    • 如何内联递归函数?
    • 解开递归结,使用inline展开。
    猜你喜欢
    • 1970-01-01
    • 2016-06-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-31
    • 1970-01-01
    相关资源
    最近更新 更多