用 C++ 编写性能关键的 C# 代码
Posted
技术标签:
【中文标题】用 C++ 编写性能关键的 C# 代码【英文标题】:Writing performance critical C# code in C++ 【发布时间】:2011-04-09 21:06:58 【问题描述】:我目前正在编写一些性能关键代码,并且我有一种特殊情况,我很想用 C# 编写整个应用程序,但性能原因意味着 C++ 最终要快得多。
我对一些代码的两种不同实现(一个在 C# 中,另一个在 C++ 中)进行了一些基准测试,时间显示 C++ 版本快 8 倍,两个版本都处于发布模式并启用了所有优化。 (其实C#有编译成64位的优势。我忘了在C++的时候启用这个)
所以我想,我可以用 C# 编写大部分代码库(C# 使得编写非常容易),然后在性能至关重要的地方编写本机版本。我在 C# 和 C++ 中测试的特定代码段是花费超过 95% 处理时间的关键领域之一。
但是,在这里编写本机代码的推荐智慧是什么?我从未编写过调用本机 C++ 的 C# 应用程序,所以我不知道该怎么做。我希望以一种尽可能减少进行本机调用的成本的方式来做到这一点。
谢谢!
编辑:下面是我实际尝试处理的大部分代码。这是一个n体模拟。 95-99% 的 CPU 时间将花在 Body.Pairwise() 中。
class Body
public double Mass;
public Vector Position;
public Vector Velocity;
public Vector Acceleration;
// snip
public void Pairwise(Body b)
Vector dr = b.Position - this.Position;
double r2 = dr.LengthSq();
double r3i = 1 / (r2 * Math.Sqrt(r2));
Vector da = r3i * dr;
this.Acceleration += (b.Mass * da);
b.Acceleration -= (this.Mass * da);
public void Predict(double dt)
Velocity += (0.5 * dt) * Acceleration;
Position += dt * Velocity;
public void Correct(double dt)
Velocity += (0.5 * dt) * Acceleration;
Acceleration.Clear();
我还有一个类,它只使用以下方法驱动模拟:
public static void Pairwise(Body[] b, int n)
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
b[i].Pairwise(b[j]);
public static void Predict(Body[] b, int n, double dt)
for (int i = 0; i < n; i++)
b[i].Predict(dt);
public static void Correct(Body[] b, int n, double dt)
for (int i = 0; i < n; i++)
b[i].Correct(dt);
主循环看起来像:
for (int s = 0; s < steps; s++)
Predict(bodies, n, dt);
Pairwise(bodies, n);
Correct(bodies, n, dt);
以上只是我实际正在开发的大型应用程序的最低限度。还有更多的事情发生,但对性能最关键的事情发生在这三个函数中。我知道成对函数很慢(它是 n^2),而且我确实有其他更快的方法(Barnes-hutt 一个,它是 n log n)但这超出了我在此要求的范围问题。
C++ 代码几乎相同:
struct Body
public:
double Mass;
Vector Position;
Vector Velocity;
Vector Acceleration;
void Pairwise(Body &b)
Vector dr = b.Position - this->Position;
double r2 = dr.LengthSq();
double r3i = 1 / (r2 * sqrt(r2));
Vector da = r3i * dr;
this->Acceleration += (b.Mass * da);
b.Acceleration -= (this->Mass * da);
void Predict(double dt)
Velocity += (0.5 * dt) * Acceleration;
Position += dt * Velocity;
void Correct(double dt)
Velocity += (0.5 * dt) * Acceleration;
Acceleration.Clear();
;
void Pairwise(Body *b, int n)
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
b[i].Pairwise(b[j]);
void Predict(Body *b, int n, double dt)
for (int i = 0; i < n; i++)
b[i].Predict(dt);
void Correct(Body *b, int n, double dt)
for (int i = 0; i < n; i++)
b[i].Correct(dt);
主循环:
for (int s = 0; s < steps; s++)
Predict(bodies, n, dt);
Pairwise(bodies, n);
Correct(bodies, n, dt);
还有一个 Vector 类,它的工作原理就像一个常规的数学向量,为简洁起见,我不包括在内。
【问题讨论】:
C# 不应该更慢。您可以使用未经检查的块来实现更快的代码,以避免溢出检查和其他东西。 ***.com/questions/5326269/… @Yochai:我已经尝试过,将我所有的算术都包装在不安全的块中。它归结为一些做大量浮点数学的函数,但我每秒都在做大量的计算。另外,我想知道如何从 C# 调用 C++,因为我已经有一些用 C++ 编写的现有代码。我想编写的一些较新的代码与计算无关,但更容易用 C# 编写。 @Yochai:我不太确定未检查代码是否比已检查代码快。虽然我承认我无法解释原因,但我目睹了它一直略微变慢的情况。 @Mike Bantegui,这是一个老问题,但您应该查看 C++ AMP 以进行 N Body 模拟。通过让它在 GPU 硬件上运行,它可以提供更好的性能提升。应该很简单,因为您已经在调用非托管代码。 【参考方案1】:您需要与本机代码交互。你可以把它放在一个DLL中并pinvoke。好的,当您不经常转换并且界面很薄时。最灵活和最快速的解决方案是用 C++/CLI 语言编写一个 ref 类包装器。看看this magazine article的介绍。
最后但同样重要的是,您确实应该分析 C# 代码。 8 倍是相当多的。在您至少了解为什么它那么慢之前,请不要开始这样做。您不想在 C++ 代码中重现原因,那样会毁掉一周的工作。
并提防错误的直觉。 64 位代码不实际上更快,它通常比 x86 代码慢一点。它有一堆额外的寄存器,非常很好。但是所有指针的大小都是双倍的,并且您不会获得双倍的 cpu 缓存。 .
【讨论】:
我重新计时,确保输入数据相同,但仍然慢了 5 倍。有什么我应该注意的事情会很容易降低 C# 的性能吗? 通过简单地重做时序几乎使性能翻倍在我的书中将是一个危险信号。没有用于优化 C# 代码的通用剧本,只有好的分析器可以向您展示周期的去向。对 Debug 构建进行概要分析或附加调试器时存在明显错误。 我认为“两倍快”来自于我跑过 Visual Studio 的事实。对于微小的计算,我可以始终如一地获得 500 毫秒(C#)和 2000 毫秒(C++)的时间。我进行了分析并确认了一种方法,其中所有周期都被花费了,这在两个平台上都是相同的,并且具有相同的 CPU 时间百分比 (99%)。 @Mike Bantegui:汉斯是对的。你必须找出真正发生了什么。当然,当我听到“周期去哪里”和“所有周期都花在哪里”时,我会立即产生怀疑,因为最浪费时间的往往是看似无辜甚至看不见的函数调用,它们伪装自己通过将时间花在其他地方。我会鼓励你沉下心来——在指令级别上一步一步,或者做this。【参考方案2】:您有两个选择:P/Invoking 和 C++/CLI。
P/调用
通过使用 P/Invoke 或 Platform Invoke,.NET(以及 C#)可以调用非托管代码(您的 C++ 代码)。这可能有点让人不知所措,但绝对有可能让您的 C# 代码调用对性能至关重要的 C++ 代码。
一些 MSDN 链接可帮助您入门:
Consuming Unmanaged DLL Functions Interop Marshaling DllImportAttribute基本上,您将创建一个 C++ DLL,该 DLL 定义了您想从 C# 调用的所有非托管函数。然后,在 C# 中,您将使用 DllImportAttribute
将该函数导入 C#。
例如,您有一个 C++ 项目,该项目创建了具有以下功能的 Monkey.dll:
extern "C" __declspec(dllexport) void FastMonkey();
然后您将在 C# 中进行如下定义:
class NativeMethods
[DllImport("Monkey.dll", CallingConvention=CallingConvention.CDecl)]
public static extern void FastMonkey();
然后您可以通过调用NativeMethods.FastMonkey
在 C# 中调用 C++ 函数。
几个常见的陷阱和注意事项:
花时间学习互操作封送处理。理解这一点将极大地帮助创建正确的 P/Invoking 定义。 默认调用约定为 StdCall,但 C++ 将默认为 CDecl。 默认字符集是 ANSI,因此如果您想编组 Unicode 字符串,则必须更新您的DllImport
定义(请参阅 MSDN - DllImport.CharSet 文档)。
http://www.pinvoke.net/ 是了解如何 P/Invoke 标准 Windows 函数调用的有用资源。如果您知道类似的 Windows 函数调用,您也可以使用它来了解如何编组某些内容。
C++/CLI
C++/CLI 是 Microsoft 创建的一系列 C++ 扩展,用于使用 C++ 创建 .NET 程序集。 C++/CLI 还允许您将非托管代码和托管代码混合到一个“混合”程序集中。您可以创建一个 C++/CLI 程序集,其中包含您的性能关键代码和您想要的任何围绕它的 .NET 类包装器。
有关 C++/CLI 的更多信息,我建议从 MSDN - Language Features for Targeting the CLR 和 MSDN - Native and .NET Interoperability 开始。
我建议您从 P/Invoking 路线开始。我发现明确区分非托管代码和托管代码有助于简化事情。
【讨论】:
【参考方案3】:在 C# 中,Vector 是类还是结构?我怀疑这是一个类,而 Arthur Stankevich 的观察结果一针见血,你可能会分配其中的许多。尝试将 Vector 设为结构,或重用相同的 Vector 对象。
【讨论】:
是的,我也在想同样的事情。我多次看到一个 Vector 类型是用 Java 方式实现的,这会导致大量的分配。而在 C++ 中,Vector 的操作大部分可以内联。为了达到与 C++ 相当的性能,Vector 类型应该实现为 struct,并且更喜欢作为操作的 ref 参数传递而不是使用重载的运算符,这样可以更好地通过 jitter 内联。【参考方案4】:最简单的方法是创建 C++ ActiveX dll。
然后您可以在 C# 项目中引用它们,Visual Studio 将创建将包装 ActiveX COM 对象的互操作。
您可以像 C# 代码一样使用互操作代码,无需额外的包装代码。
有关 AciveX/C# 的更多信息:
Create and Use a C++ ActiveX component within a .NET environment
【讨论】:
这是最有效的方法吗?或者 P/Invoke 会更有效吗?我不介意一种方法更难,只要这意味着我可以提取尽可能多的性能。 这很容易做到。我认为通信开销没有区别。【参考方案5】:“我在两个方面做了一些基准测试 一些代码的不同实现 (一个在 C# 中,另一个在 C++ 中)和 时序显示 C++ 版本 快了 8 倍”
我用 C#、C++、Java 和一点 F# 做了一些数值计算,C# 和 C++ 之间的最大差异是 3.5。
分析你的 C# 版本并找到瓶颈(可能存在一些与 IO 相关的问题,不必要的分配)
【讨论】:
没有 IO 瓶颈(除了加载测试数据之外没有实际的 IO),我重新检查了代码,我可以确认有一种方法(在 C# 和 C++ 上)除了原始之外什么都不做计算,并且占用了 99% 的时间。 当然,我可以发布已知昂贵的特定部分。这只不过是一个重力模拟。给我几分钟,我会更新我的主要帖子。【参考方案6】:对于简单的情况,P/Invoke 绝对比 COM Interop 更容易。但是,如果您在 C++ 中执行更大的类模型块,您可能真的需要考虑 C++/CLI 或 COM 互操作。
ATL 使您可以立即创建一个类,并且一旦对象被实例化,调用开销基本上与 P/Invoke 一样小(除非您使用动态调度,IDispatch,但这应该很明显)。
当然,C++/CLI 是最好的选择,但这并不适用于所有地方。 P/Invoke 可以在任何地方工作。 COM 互操作是supported on Mono up to degree
【讨论】:
【参考方案7】:看起来您在代码中做了很多隐式 Vector 类分配:
Vector dr = b.Position - this.Position;
...
Vector da = r3i * dr;
this.Acceleration += (b.Mass * da);
b.Acceleration -= (this.Mass * da);
尝试重用已分配的内存。
【讨论】:
以上是关于用 C++ 编写性能关键的 C# 代码的主要内容,如果未能解决你的问题,请参考以下文章