用C ++编写性能关键的C#代码

时间:2011-04-09 21:06:58

标签: c# c++ performance

我目前正在研究一些性能关键代码,我有一个特殊情况,我喜欢用C#编写整个应用程序,但性能原因意味着C ++最终会更快地实现FAR。

我对一些代码的两个不同实现做了一些基准测试(一个用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为1,这是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类,它就像一个常规的数学向量一样,为简洁起见我不包括这个。

7 个答案:

答案 0 :(得分:8)

您需要与本机代码进行交互。你可以把它放在DLL和pinvoke中。好的,当你不经常转换,界面很薄。最灵活,最快速的解决方案是使用C ++ / CLI语言编写ref类包装器。请查看this magazine article的介绍。

最后但并非最不重要的是,你真的应该分析C#代码。 8倍是非常过分的。不要开始这个,直到你至少知道为什么 慢。你不想在C ++代码中重复原因,这会毁掉一周的工作。

谨防错误的本能。 64位代码实际上更快,它通常比x86代码慢一点。它有一堆额外的寄存器,非常很好。但是所有的指针都是大小的两倍,你不会得到双倍的cpu缓存。

答案 1 :(得分:2)

您有两种选择:P / Invoking和C ++ / CLI。

<强> P /调用

通过使用P / Invoke或Platform Invoke,.NET(以及C#)可以调用非托管代码(您的C ++代码)。它可能有点压倒性,但绝对有可能让您的C#代码调用性能关键的C ++代码。

一些MSDN链接可帮助您入门:

基本上,您将创建一个C ++ 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 ++函数。

几个常见的问题和注意事项:

  • 花时间学习Interop Marshaling。理解这将极大地有助于创建正确的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 CLRMSDN - Native and .NET Interoperability开始。

我建议你从P / Invoking路线开始。我发现在非托管代码和托管代码之间有明确的分离有助于简化事情。

答案 2 :(得分:1)

在C#中,Vector是一个类还是结构?我怀疑这是一个班级,亚瑟·斯坦克维奇(Arthur Stankevich)发现他可能正在分配许多这些东西。尝试使Vector成为一个结构,或重用相同的Vector对象。

答案 3 :(得分:0)

最简单的方法是创建C ++ ActiveX dll。

然后你可以在C#项目中引用它们,Visual Studio将创建将包装ActiveX COM对象的interops。

您可以使用互操作代码,如C#代码,无需额外的包装代码。

有关AciveX / C#的更多信息:

Create and Use a C++ ActiveX component within a .NET environment

答案 4 :(得分:0)

  

“我对两个人做了一些基准测试   一些代码的不同实现   (一个在C#中,另一个在C ++中)和   时间显示了C ++版本   “快了8倍”

我在C#,C ++,Java和一些F#中进行了一些数值计算,C#和C ++之间最大的差异是3.5。

描述您的C#版本并找到瓶颈(可能存在一些与IO相关的问题,不必要的分配)

答案 5 :(得分:0)

对于简单的情况,P / Invoke肯定比COM Interop更容易。但是,如果你在C ++中做更大的类模型,你可能真的想要考虑C ++ / CLI或COM Interop。

ATL让你立刻掀起一个类,一旦对象被实例化,调用开销基本上与P / Invoke一样小(除非你使用动态调度,IDispatch,但这应该是显而易见的)。 / p>

当然,C ++ / CLI是最好的选择,但这并不适用于所有地方。 P / Invoke可以在任何地方工作。 COM互操作是supported on Mono up to degree

答案 6 :(得分:0)

看起来你在代码中进行了很多隐式的Vector类分配:

Vector dr = b.Position - this.Position;
...
Vector da = r3i * dr;
this.Acceleration += (b.Mass * da);
b.Acceleration -= (this.Mass * da);

尝试重新使用已分配的内存。