【问题标题】:Manually implementing high performance algorithms in .NET在 .NET 中手动实现高性能算法
【发布时间】:2009-11-13 05:18:58
【问题描述】:

作为学习经验,我最近尝试在 C# 中实现 Quicksort with 3 way partitioning。

除了需要在递归调用之前对左/右变量添加额外的范围检查之外,它似乎工作得很好。

我事先知道框架在 List.Sort 中提供了一个built-in Quicksort implementation(通过 Array.Sort)。所以我尝试了一些基本的分析来比较性能。结果:内置的 List.Sort 方法,对相同的列表进行操作,比我自己手动实现的速度快 10 倍左右。

使用反射器,我发现 List.Sort 中的实际排序是在外部代码中实现的,而不是 IL(在名为 tryszsort() 的函数中)。

看看我自己的快速排序实现,我希望用迭代替换递归调用可能会带来一些改进。此外,禁用数组边界检查(如果可能)也可以带来一些好处。也许这会更接近内置实现,但我没有信心。

所以我的问题是:期望优化算法(用 .NET IL 编写,jitted 本机代码)的性能可以与外部实现算法的性能竞争是否现实?

我再次意识到 Quicksort 是作为框架的一部分提供的,这对我来说只是一次学习经历。然而,还有许多算法(想到 CRC32)没有提供,但对许多应用程序仍然具有很大的价值。这是关于implementing CRC32 in .NET 和性能问题的相关问题。

那么如果您需要在 .NET 中实现这样的算法,需要了解哪些主要的性能注意事项,以便您的算法至少可以接近外部代码的性能?

[更新]

通过将算法更改为对简单的 Int 数组而不是 List 进行操作,我已将执行速度提高到内置 Array.Sort 的 10% 以内。在 Reflector 中,我可以看到这避免了对列表中的每个 get 或 set 执行 Callvirt() 操作。我认为这可能会有所改善,但我对它的效果感到惊讶。

【问题讨论】:

  • 我一直认为大多数快速排序实现会在递减预定数量的递归级别后切换到合并排序。您介意发布您的代码以查看需要改进的地方吗?

标签: .net performance algorithm quicksort


【解决方案1】:

通过使用非递归代码,尤其是通过使用“不安全”块和指针算法(如果适用),您可以实际上看到用 C# 编写的算法获得 x5 或 x10 的性能提升。 与往常一样,性能(在处理托管环境时更是如此),除非您尝试并对其进行基准测试,否则您永远不会完全知道。

现在,一般来说,你应该主要用 C# 编写东西,然后对其进行优化,再优化一些,如果仍然不够好,确定确切的关键代码段并将其移植到 C++(同时要小心限制托管/本地调用边界的数量)。

【讨论】:

  • 不安全模式的有趣点。默认情况下,我试图避免这种情况,但在某些情况下,指针算法可能是唯一的选择。我完全同意优化方法,我也尽量不陷入微优化;改进整体设计可能会带来更好的方法改进。
  • +1,但您能否详细说明未经检查的块如何提高性能?因为 unchecked 是默认的,而且我认为它对数组边界检查没有任何影响。
  • 请注意,移植到 C++ 并不总能带来性能优势,因为调用约定不匹配,通过 P/Invoke 的托管到非托管调用会受到惩罚,而且事实上cdecl 和 stdcall 都将参数推送到注册。 BCL 中的大多数extern 方法都使用“快速”调用约定,该约定依赖于实现该方法的本机代码,了解 CLR 的实现细节,因此它们不会为此付出代价。
  • 好点 Pavel。这就是为什么我说要小心本地/托管边界,但我没有解释它。 Julian,我不认为“未选中”是默认设置,因为如果使用不正确的索引,则会出现数组越界异常。
  • 数组总是被边界检查,除非编译器可以推断出它没有必要或者如果你使用指针算法。未经检查的块在这里没有效果 AFAIK。我所说的“默认未经检查”的意思是,默认情况下原始类型的溢出不会生成异常,除非您将其包装在 checked 块中或使用编译器选项。当使用编译器选项时,unchecked 允许阻止行为像默认值。除非我弄错了,unchecked 在性能优化中没有任何作用。
【解决方案2】:

出于好奇,尽管我有 9 年的 .NET 经验,但我仍然经常犯这个错误:您是否在发布模式下编译代码并进行了优化?调试代码的性能明显低于优化的发布代码。

假设您在发布模式下进行编译,如果您以类似方式实现算法(即迭代与迭代或递归与递归),性能应该不会有很大差异。如果您希望查看 .NET 实现并弄清楚,您可以下载 SSCLI,Share-Source Common Language Infrastructure。这是 Microsoft 公开可用的符合 ECMA 标准的 CLI 实施。它不是我们都知道和喜爱的 .NET 框架的 100%,但它是其中的重要部分。它可以提供很多 Reflector 无法提供的信息,包括内部实现。所有类型的代码都可用,包括 C#、C++,甚至在某些情况下还包括一些汇编程序。

【讨论】:

  • 是的,我在 Release 模式下编译,但是我没有明确检查任何优化,我认为设置 Release 就足够了。我得看看这个,所以谢谢你的提示。也感谢 aso 提供到 SSCLI 的链接。看看是否/如何实现排序很有趣。
  • “调试代码的性能明显低于优化的发布代码。” - 实际上,对于 .Net 而言并非如此;两者的性能通常是可比的。真正重要的是您是否附加了调试器 - JIT(执行大部分优化)在附加调试器时(即在 Visual Studio 中运行时)默认情况下不会优化。您可以在Tools->Options->Debugging->Supress JIT Optimization下的VS中更改此设置@
  • @BlueRaja:实际上并非如此。 JIT 优化是一个程序集元数据标志,它完全禁用优化,调试或其他方式。我已经进行了一些相当广泛的测试,特别是使用 WCF 和 ASP.NET,使用调试和发布模式,没有附加调试器。调试模式(禁用 JIT 优化)明显变慢。例如,一个简单的 WCF 计算器服务(add、sub、div、mul)在启用 Debug 模式时以 ~180msg/s 的速度运行...在启用 Release 模式时以 ~30,000msg/s 的速度运行。没有附加调试器。与 ASP.NET 的某些方面存在类似差异。
  • 是的,抱歉,我说得太半开玩笑了。我的意思是,真正重要的是是否启用了 JIT 优化器。 在附加调试器时默认禁用此功能。它在 Debug 构建中部分启用,但可以通过一些 .ini flags 完全重新启用。但是,WCF 在调试构建中如此慢得多的原因与此无关 - 当找到 DEBUG 预处理器符号时,WCF 添加诊断代码/消息。它还使用 VS WCF 服务主机(而不是 IIS),它只用于开发。
  • 嗯,我们的测试当时托管在 IIS 7.0 中……但我认为调试过程中缓慢的原因并不重要。我知道很多 Microsoft 代码在启用调试“模式”(以及所有编译器标志和随之而来的其他设置)时涉及大量跟踪和其他诊断垃圾。因为绝大多数 .NET 开发人员代码都是基于在 .NET Framework 中,无论何时调用它,Microsoft 的所有诊断垃圾都会被编入您自己的代码中。确保为最终产品构建发布代码的原因有很多……否则性能命中可能会很大!
【解决方案3】:

确保您是在比较苹果和苹果。

排序时,Compare 函数可能占主导地位,这可能在实现之间有所不同。

假设这两种情况下的比较函数都足够快而不会成为问题,那么时间可能会被数组边界检查之类的东西所支配,这很容易产生很大的不同。

【讨论】:

  • 好提示。在这种情况下,我目前只使用基本的 Int32 比较。我确实发现对简单的 int 数组进行排序比 List 快得多。 List 上的每个 get 和 set 似乎都有一个“Callvirt”。通过使用数组删除它,(至少)执行时间减半。
猜你喜欢
  • 2015-12-03
  • 2015-10-05
  • 2021-12-24
  • 2016-02-01
  • 2011-09-13
  • 1970-01-01
  • 1970-01-01
  • 2018-10-15
  • 2011-08-12
相关资源
最近更新 更多