用C/C++重写C#代码的性能提升

17

我写了一个在C#中处理字符串的程序部分。我最初选择C#不仅因为使用.NET的数据结构更容易,而且因为我需要使用这个程序分析数据库中的大约2-3百万个文本记录,并且使用C#连接到数据库更加容易。

程序中有一部分导致整个代码速度变慢,所以我决定重新用C语言编写这一部分,使用指针来访问字符串中的每个字符,现在这部分代码只需要5秒就可以完成10,000,000个字符串的分析,而之前使用C#时需要119秒!性能是关键,所以我正在考虑将整个程序重写为C语言,将其编译为dll(当我开始编写程序时我不知道如何这样做),并使用从C#调用DllImport方法来使用该dll的方法处理数据库字符串。

鉴于重写整个程序需要一些时间,而使用DllImport来处理C#字符串需要进行类似封送等操作,我的问题是:使用C dll更快的字符串处理性能是否超过从C#反复调用该dll所需的性能损耗?


18
我认为你应该展示一下你在C#中如何处理字符串。虽然C程序确实应该更快,但两者之间不应该有如此巨大的性能差异。 - Pablo Santa Cruz
10
如果您使用StringBuilder可能可以加快C#原始代码的速度。没有看到任何代码很难说。 - Henrik
5
你尝试过使用分析工具来查看你的C#代码中实际的性能瓶颈吗? - Kos
5
@Pablo、@Henrik和@Albin:我观察到类似的性能问题。你不能创建指向字符串的指针,所以访问子字符串的唯一方便方式是在堆上创建一个新的字符串。如果你的代码从大量文本中提取单词,可能会创建数百万个短暂的微小对象,这会导致相当大的GC压力。即使使用字符串驻留也没有帮助,因为你必须先创建字符串(在堆上),然后才能要求系统对其进行驻留。 - Marcelo Cantos
2
@digEmAll:是的,你可以这样做,并且它会有所帮助,但由于需要检查每个字符访问的索引,仍然效率不高。同时,由于.NET类库提供的字符串处理内置函数无法操作字符范围,因此也更加复杂。 - Marcelo Cantos
显示剩余4条评论
9个回答

10

一种选择是将 C 代码重写为不安全的 C# 代码,这样应该具有大致相同的性能,并且不会产生任何交互操作的惩罚。


4
我预计性能不会相同。仍然存在动态分配等差异。 - Puppy
3
C语言的优势并不在于其动态内存分配函数malloc更快,事实上它要慢得多,而是可以大幅减少对内存分配器的调用次数。此外,访问数组元素时不会进行越界检查。C#中的不安全代码也具有相同的好处。 - Marcelo Cantos
@Marcelo:我知道malloc非常慢,但它肯定不同于gcnew。 - Puppy
@Marcelo:性能更差并不等于没有性能。我的观点是,在不安全的C#代码和实际本地代码之间存在数十个因素。 - Puppy
根据我的经验,在处理小对象时,malloc的速度要慢得多。但是在C++中更容易分配内存块并重用它们。当然,C#不安全操作并不能改变编译器和库之间巨大的差异;实际上,使用模板的C++比C#更加智能。 - Eamon Nerbonne
显示剩余2条评论

10

首先对代码进行性能分析。你可能会发现一些简单易懂的优化方法,从而大大提高C#代码的运行速度。

其次,使用指针编写C代码并不是一个公平的比较。如果你要使用指针,为什么不直接用汇编语言来获取更好的性能呢?(实际上这并不可取,这只是一种归谬法。)与本机代码进行更好的比较方法是使用 std::string。这样你仍然可以从 string 类和 C++ 异常处理机制中获得很多帮助。

考虑到你需要从数据库中读取 200-300 万条记录才能完成该工作,我非常怀疑解密字符串所花费的时间是否比从数据库中加载数据所花费的时间还要多。因此,你应该考虑如何组织你的代码,以便在数据库加载过程中开始字符串处理。

如果你使用一个 SqlDataReader(例如)按顺序加载行,就应该可以尽可能快地将 N 行打包,并将其传递给一个单独的线程进行后处理,这是你当前头痛的问题,也是你提出这个问题的原因。如果你使用 .Net 4.0,最简单的方法是使用 Task Parallel Library,而System.Collections.Concurrent也可以在线程之间对结果进行整理。

这种方法意味着无论是数据库延迟还是字符串处理都不会成为拖累,因为它们是并行进行的。即使你在单处理器机器上,应用程序仍然可以在等待下一批通过网络从数据库返回的数据时处理字符串。如果发现字符串处理是最慢的,则可以为其使用更多线程(即Tasks)。如果数据库是瓶颈,则必须考虑外部手段来提高其性能-数据库硬件或架构,网络基础设施。如果需要在处理更多数据之前得到一些结果,则 TPL 允许在 Tasks 和协调线程之间创建依赖关系。

我的观点是,我怀疑重新设计整个应用程序为本机 C 或其他语言而付出的代价是否值得。有很多方法可以解决此问题。


@Grigory - 谢谢。通常这是首要的问题...无论如何,即使当前代码在孤立状态下看起来是最优的,并行化设计也应该有所帮助。 - Steve Townsend
@Steve:如果问题是由于数百万个小分配引起的GC压力,那么并行实现也无济于事。每个线程都会被垃圾收集器卡住。 - Marcelo Cantos
@Marcelo - 谢谢,这就是我说先处理配置文件,其次再优化的原因。 - Steve Townsend
@Steve:我知道。我是在回应你的评论,而不是你的答案。此外,在进行性能分析之前,可以做出一些普遍观察是可能的。 - Marcelo Cantos
@Marcelo - 是的,我在评论中未明确说明的假设是最优实现不会像您描述的那样对GC施加压力,但这当然并非普遍适用。这取决于要解决的问题。 - Steve Townsend
并行化是20倍效率差异的很差的替代品。如果你的CPU受限,并且将少量代码移植到C++可以产生20倍的差异,那么这几乎肯定比并行化代码更好 - 用户不仅没有20个核心(但),并行化还有开销 - 这些核心实际上可以做一些有意义的事情。(在这个例子中,涉及到DB,我很难相信CPU会受限,除非有一些奇怪的事情发生)。 - Eamon Nerbonne

4
这里已经有一些很好的回答了,特别是@Steve Townsend的回答。
然而,我认为值得强调一个关键点:从本质上讲,C代码“比C#代码更快”的理由是不存在的。这个想法是一种神话。它们都会产生在同一CPU上运行的机器码。只要你不要让C#做更多的工作,那么它就可以表现得和C一样好。
通过切换到C,你强迫自己更加节俭(避免使用高级功能,如托管字符串、边界检查、垃圾收集、异常处理等,并将字符串简单地视为原始字节块)。如果你将这些低级技术应用于你的C#代码(即像在C中一样将你的数据视为原始字节块),你会发现速度上的差异要小得多。
例如:上周我重新编写了(用C#编写的)一个初级程序员编写的类。我通过应用与我在C中编写时相同的方法(即考虑性能)获得了25倍的速度提升。我实现了你所声称的相同的速度提升,而根本不需要改变语言。
最后,仅仅因为一个孤立的案例可以使性能提高24倍,并不意味着你可以通过将整个程序全部转换为C来使其在所有方面都提高24倍。正如Steve所说,对其进行剖析以找出它的缓慢之处,并仅在能够提供显著收益的地方花费精力。如果你盲目地转换为C,你可能会发现你已经花费了很多时间使一些已经工作良好的代码变得难以维护。
(附注:我的观点来自于29年的汇编、C、C++和C#编程经验,并且理解语言只是生成机器码的工具——在C#与C++与C之间,主要是程序员的技能而不是使用的语言决定代码运行快或慢。C/C++程序员往往比C#程序员更优秀,因为他们必须这样做——C#允许你变得懒惰并快速编写代码,而C/C++则需要你做更多的工作,代码编写时间更长。但是一个好的程序员可以在C#中获得出色的性能,而一个糟糕的程序员可以从C/C++中挣扎出糟糕的性能。)

4

没有理由用C来代替C ++,也不存在C / C ++。

关于封送的性能影响非常简单。如果您必须逐个封送每个字符串,则性能将非常低下。如果您可以在一个调用中封送所有一千万个字符串,则封送不会产生任何差别。P / Invoke不是世界上最快的操作,但如果您只调用它几次,那么性能就不会有太大影响。

重写核心应用程序并使用C ++ / CLI将其与C#数据库端合并可能更容易。


2

在.NET中,由于字符串是不可变的,我毫不怀疑一个经过优化的C实现将会比一个经过优化的C#实现表现更好 - 毫无疑问!

P/Invoke确实会带来一些开销,但如果你在C中实现大部分逻辑,并且仅为C#暴露非常详细的API,我相信你会处于更好的状态。

归根结底,在C中编写实现意味着需要更长的时间 - 但如果您为额外的开发成本做好了准备,这将会给您带来更好的性能。


1
OP表示该应用程序正在分析字符串,因此假设这主要涉及读取操作,则不可变性可能不是罪魁祸首。 - Brian Rasmussen
2
我严重怀疑一个优化的C#版本会更慢。根本限制是RAM总线带宽。只是更难优化C#代码,因为字符串喜欢创建副本并检查数组索引。 - Hans Passant
我毫不怀疑。通过使用字符串 shim 和小字符串优化,您可以在使用更少的 RAM 带宽的情况下完成更多工作。 - MSalters
@Hans:这取决于你如何优化C#。如果你使用不安全的代码块,你可以达到C语言的性能水平,但是如果你坚持使用安全的C#,你将会遇到一些基本上无法突破的硬性限制。 - Marcelo Cantos
@Marcelo - 在C#中使用指针确实可以简单地绕过索引检查。为什么不能呢?毕竟你不是在与安全的C竞争。 - Hans Passant
@Hans:如果这段代码需要在SQL Server中运行,那么不安全的C#选项就不可行了。因地制宜。 - Marcelo Cantos

2

熟悉混合程序集比Interop更好。Interop是处理本地库的快速方法,但混合程序集的性能更好。
MSDN上的混合程序集
像往常一样,最重要的是测试和测量...


0

对于长字符串或多个字符串的连接,总是使用StringBuilder。但不是每个人都知道,StringBuilder不仅可以用于使字符串连接更快,还可以用于插入、删除和替换字符。

如果这还不够快,您可以使用char-或byte数组代替字符串进行操作。完成操作后,您可以将数组转换回字符串。

C#中还有一个选项,即使用不安全代码获取指向字符串的指针并修改本来是不可变的字符串,但我真的不推荐这样做。

正如其他人已经说过的,您可以使用托管C++(C++/CLI)在.NET和托管代码之间进行良好的互操作。

能否为我们展示一下代码,也许还有其他优化选项?


0

当你在晚期开始优化程序时(应用程序编写时没有考虑优化),那么你必须确定瓶颈所在。

分析是查看所有 CPU 周期去向的第一步。

请记住,C# 分析器只会分析您的 .Net 应用程序 -而不是实现在内核中的 IIS 服务器或网络堆栈。

这可能是一个隐形的瓶颈,比你试图取得进展时关注的问题要高几个数量级。

你认为你对作为内核驱动程序实现的 IIS 没有影响力 -而且你是正确的。

但是你可以不使用它 -并节省大量时间和金钱。

把你的才华放在可以产生差异的地方 -而不是被迫双脚绑在一起奔跑的地方。


这个话题与IIS无关,但出于某种原因,你总是提到g-wan。 我看了一下那个失败得很快的C#代码,第一件事就是注意到滥用字符串。http://www.g-wan.com/source//loan.aspx.txt - ZippyV
我同意ZippyV的观点。原始问题是关于原生C#和C或C++替代方案以及数据库访问的。我想知道Jerome是不是产品粉丝,还是在尝试某种产品推广... - paercebal

0

通常认为,固有的差异是CPU少2倍,内存少5倍。实际上,很少有人精通C或C++以获得这些好处。

如果省略Unicode支持,还可以获得额外的收益,但只有您能够充分了解您的应用程序,才能知道是否安全。

首先使用分析器,确保您没有受到I/O限制。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接