用C编写DLL文件来加速C#中的数学代码?

12

我有一个非常庞大的嵌套for循环,其中对浮点数进行了一些乘法和加法运算。

for (int i = 0; i < length1; i++)
{
    double aa = 0;
    for(int h = 0; h < 10; h++)
    {
       aa += omega[i][outsideGeneratedAddress[h]];
    }

    double alphaOld = alpha;
    alpha = Math.Sqrt(alpha * alpha + aa * aa);

    s = -aa / alpha;
    c = alphaOld / alpha;

    for(int j = 0; j <= i; j++)
    {
        double oldU = u[j];
        u[j] = c * oldU + s * omega[i][j];
        omega[i][j] = c * omega[i][j] - s * oldU;
    }
}
这个循环占据了我大部分的处理时间,并成为瓶颈。
如果我将此循环改写为C语言并从C#进行接口调用,是否可能会看到任何速度提升?
编辑:我更新了代码以显示如何生成s和c。此外,内部循环实际上从0到i,尽管这可能对问题没有太大影响。
编辑2:我在VC++中实现了该算法,并通过dll与C#链接,在启用所有优化时比C#快28%。启用SSE2参数特别有效。使用MinGW和gcc4.4编译只提高了15%的速度。刚刚试过英特尔编译器,发现该代码的速度提升了49%。

3
在C#中,浮点运算与C语言一样快。可能只有数组边界检查会稍微降低C#的速度。您可以使用不安全代码来消除这个问题。只有当您的C代码编译为SIMD(或类似)指令时,才能看到显着的改进。但调用本地代码会带来一些成本,这种改进应该是值得的。如果您发布更多代码(GetS、GetC),我们可能会帮助您加速代码。 - dtb
2
你也许可以通过考虑二维矩阵的引用局部性来获得更快的速度...正如@dtb所说,浮点运算在两种语言中都很快。 - Mitch Wheat
长度1和长度2的一般范围是多少?只是出于好奇。 - Rusty
每个循环大约1000到100000。可能看起来不多,但我会每秒运行许多次嵌套循环,并且需要确保每次运行都在一定的毫秒数内完成。 - Projectile Fish
我会研究直接使用SIMD(http://tirania.org/blog/archive/2008/Nov-03.html)访问,而不是编写C代码。 - Roman
显示剩余4条评论
12个回答

8

更新:

如果您编写内部循环以考虑引用的局部性,会发生什么:

for (int i = 0; i < length1; i++) 
{ 
    s = GetS(i); 
    c = GetC(i); 
    double[] omegaTemp = omega[i]; 

    for(int j = 0; j < length2; j++) 
    { 
        double oldU = u[j]; 
        u[j] = c * oldU + s * omegaTemp[j]; 
        omegaTemp[j] = c * omegaTemp[j] - s * oldU; 
    } 
} 

啊,我了解了,谢谢。引用的局部性效果还是不错的。但是为什么编译器不能自动识别呢? - Projectile Fish
4
如果我没记错的话,C#编译器在处理相对简单的for循环时也可以进行边界检查优化。Mitch的建议可能也促进了这种优化。http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspx - Josh
2
如果边界检查确实被优化掉了,我不会指望使用不安全的代码能够带来更好的性能。 - Josh
1
糟糕!太晚了无法编辑。在Eric Lippert批评我之前,我应该指出上面说错了……这不是C#的优化,而是JIT的优化。因此它并不特定于C#。 - Josh
@Josh:发现得好...也很好知道。我想你避免了激怒Lippert的情况 :) - Rusty
显示剩余2条评论

7

使用unsafe块和指针来索引omega数组。这将消除范围检查的开销,并且如果您进行足够的访问,可能会获得显着的优势。您的GetS()GetC()函数也可能花费大量时间,但您没有提供源代码。


我对这段代码进行了分析,发现GetS()和GetC()的CPU使用率与内部循环代码相比非常低。我会尝试使用不安全的代码,但是根据我的经验,使用不安全块往往会减慢我的代码速度。也许不安全代码有一个我没有意识到的开销,在大循环中不会那么显著。 - Projectile Fish
2
你可能想要发布一些你编写的不安全代码。这样做有几个原因会使它变慢。 - Rusty
4
将“fixed”语句放在循环中会使程序变慢。诀窍在于将数组修复语句仅放在最外层语句中,只修复一次。 - dtb
3
为避免频繁进入和退出不安全的上下文,因为这可能会涉及代码访问安全性问题,例如堆栈遍历以验证权限。 - Josh
添加一个unchecked{}块会有帮助吗? - Michael Stum
1
如果您在构建选项中勾选了“检查算术溢出/下溢”,那么unchecked{}才有用,不是吗? - Projectile Fish

3
你可以尝试使用Mono.Simd来更加充分地利用CPU。
话虽如此,通过手动从循环中提取重复语句,在C#中也可以获得很大的收益。 http://tirania.org/blog/archive/2008/Nov-03.html
var outsideAddr0 = outsideGeneratedAddress[0];
var outsideAddr1 = outsideGeneratedAddress[1];
var outsideAddr2 = outsideGeneratedAddress[2];
var outsideAddr3 = outsideGeneratedAddress[3];
var outsideAddr4 = outsideGeneratedAddress[4];
var outsideAddr5 = outsideGeneratedAddress[5];
var outsideAddr6 = outsideGeneratedAddress[6];
var outsideAddr7 = outsideGeneratedAddress[7];
var outsideAddr8 = outsideGeneratedAddress[8];
var outsideAddr9 = outsideGeneratedAddress[9];
for (int i = 0; i < length1; i++)
{
  var omegaAtI = omega[i];
  double aa = 
   omegaAtI[outsideAddr0]
   + omegaAtI[outsideAddr1]
   + omegaAtI[outsideAddr2]
   + omegaAtI[outsideAddr3]
   + omegaAtI[outsideAddr4]
   + omegaAtI[outsideAddr5]
   + omegaAtI[outsideAddr6]
   + omegaAtI[outsideAddr7]
   + omegaAtI[outsideAddr8]
   + omegaAtI[outsideAddr9];

  double alphaOld = alpha;
  alpha = Math.Sqrt(alpha * alpha + aa * aa);

  var s = -aa / alpha;
  var c = alphaOld / alpha;

  for(int j = 0; j <= i; j++)
  {
    double oldU = u[j];
    var omegaAtIJ = omegaAtI[j];
    u[j] = c * oldU + s * omegaAtIJ;
    omegaAtI[j] = c * omegaAtIJ  - s * oldU;
  }
}

3

运行原生C/C++代码并不能自动加速,这种想法高度不可靠。如果您擅长使用SIMD(且length1length2足够大,使得P/Invoke调用不会对性能产生显著影响),那么或许可以尝试一些优化。

但是要确定是否真的有效,只有通过尝试并进行性能分析才能得出结论。


2

仅使用C或C++并不会带来太大的速度提升,您需要进行优化。此外,调用C例程的开销也会有一定影响,除非您在循环中多次这样做。

首先尝试一些其他C#方案。如果变量是float而不是double,则会减慢计算速度。另外,如Raj所说,使用并行编程将会大大提高速度。


2

.NET与非托管代码的交互速度非常慢。你可以使用系统API分配非托管内存,以获得所有非托管内存的好处。

你可以调用VirtualAlloc来分配内存页面,然后调用VirtualProtect将它们直接固定到RAM中而不进行交换。

这种方法允许在大量数据上执行计算,速度至少比在托管内存中执行快3倍。


2
虽然其他答案都建议你考虑C#的解决方案,但大多数人忽略了一个重要点: 只要使用一个好的优化编译器(我建议使用英特尔,这种代码效果非常好),用 C 语言编写该方法将更快。
编译器还会从JIT节省一些工作,并产生更好的编译输出(即使是 MSVC 编译器也能生成 SSE2 指令)。默认情况下不会检查数组边界,可能会有一些循环展开,总的来说,您可能会看到显着的性能提升。
正如正确指出的那样,调用本机代码可能会有一些开销;但是,如果 length1 足够大,这些开销应该是微不足道的,与速度提升相比。
当然,您可以在 C# 中保留此代码,但请记住,与几个 C 编译器相比,CLR(以及我所知道的所有其他 VM)对生成的代码进行的优化很少。

或者使用Fortran并获得漂亮的数组表示法! - Erik Thysell

1

不幸的是,我无法使用并行for循环,因为GetS和GetC取决于omega的生成值。 - Projectile Fish
不知道GetS()和GetC()的实际作用,如果你不能将数组处理分割成并行计算,我会感到惊讶的。每个内部循环迭代只处理i和j的单个值。你说GetS()和GetC()的CPU使用率非常低,所以我怀疑它们是否在整个omega数组上操作。PP是我会努力的地方。 - Simon Chadwick
我刚刚编辑了代码片段,以展示s和c是如何生成的。也许它可以并行化,因为它实际上是基于系统阵列实现的代码,但我不知道如何在普通多核CPU上进行多线程处理。 - Projectile Fish

1

对于Java中的普通64位算术,当将其移植到C并调整优化标志(-fprofile-generate,-fprofile-use)时,我看到了约33%的加速(从23 ns到16 ns)。这可能是值得的。

另一件事是omega[i][j]使它看起来像是一个数组的数组 - 你可以使用二维数组获得更好的性能(我认为语法类似于omega[i,j],但我忘记了如何分配它)。


0
非常怀疑。在C#中,处理原始类型且不分配内存的内部循环将非常高效。本机字节码将从IL生成一次,因此不应该有太多托管开销。
考虑到这是一个相当小的函数,您可以对两者进行性能分析,看看是否有任何差异。

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