关于c#4.0:如何在C#中进行快速复杂运算

How to do Speedy Complex Arithmetic in C#

我现在正在研究一个C分形生成器项目,它需要很多复杂数字的算术运算,我正在努力思考加速数学运算的方法。下面是一组简化的代码,使用三种数据存储方法之一测试Mandelbrot计算的速度,如TestNumericsComplexTestCustomComplexTestPairedDoubles所示。请理解Mandelbrot只是一个例子,我希望未来的开发人员能够创建插件分形公式。

基本上,我看到使用System.Numerics.Complex是一个不错的想法,而使用一对double或自定义复杂结构是可以接受的想法。我可以使用GPU执行算法,但这不会限制或破坏可移植性吗?我试过改变内环的顺序(i,x,y),但没有用。我还能做些什么来加速内部循环?我是否遇到页面错误问题?与浮点值相比,使用定点数字系统能提高我的速度吗?

我已经知道C 4.0中的Parallel.For;为了清晰起见,代码示例中省略了它。我也知道,对于高性能来说,C语言通常不是一种好语言;我使用C语言来利用插件的反射和窗口化的WPF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
using System;
using System.Diagnostics;

namespace SpeedTest {
class Program {
    private const int ITER = 512;
    private const int XL = 1280, YL = 1024;

    static void Main(string[] args) {
        var timer = new Stopwatch();
        timer.Start();
        //TODO use one of these two lines
        //TestCustomComplex();
        //TestNumericsComplex();
        //TestPairedDoubles();
        timer.Stop();
        Console.WriteLine(timer.ElapsedMilliseconds);
        Console.ReadKey();
    }

    /// <summary>
    /// ~14000 ms on my machine
    /// </summary>
    static void TestNumericsComplex() {
        var vals = new System.Numerics.Complex[XL,YL];
        var loc = new System.Numerics.Complex[XL,YL];

        for (int x = 0; x < XL; x++) for (int y = 0; y < YL; y++) {
            loc[x, y] = new System.Numerics.Complex((x - XL/2)/256.0, (y - YL/2)/256.0);
            vals[x, y] = new System.Numerics.Complex(0, 0);
        }

        for (int i = 0; i < ITER; i++) {
            for (int x = 0; x < XL; x++)
            for (int y = 0; y < YL; y++) {
                if(vals[x,y].Real>4) continue;
                vals[x, y] = vals[x, y] * vals[x, y] + loc[x, y];
            }
        }
    }


    /// <summary>
    /// ~17000 on my machine
    /// </summary>
    static void TestPairedDoubles() {
        var vals = new double[XL, YL, 2];
        var loc = new double[XL, YL, 2];

        for (int x = 0; x < XL; x++) for (int y = 0; y < YL; y++) {
                loc[x, y, 0] = (x - XL / 2) / 256.0;
                loc[x, y, 1] = (y - YL / 2) / 256.0;
                vals[x, y, 0] = 0;
                vals[x, y, 1] = 0;
            }

        for (int i = 0; i < ITER; i++) {
            for (int x = 0; x < XL; x++)
                for (int y = 0; y < YL; y++) {
                    if (vals[x, y, 0] > 4) continue;
                    var a = vals[x, y, 0] * vals[x, y, 0] - vals[x, y, 1] * vals[x, y, 1];
                    var b = vals[x, y, 0] * vals[x, y, 1] * 2;
                    vals[x, y, 0] = a + loc[x, y, 0];
                    vals[x, y, 1] = b + loc[x, y, 1];
                }
        }
    }


    /// <summary>
    /// ~16900 ms on my machine
    /// </summary>
    static void TestCustomComplex() {
        var vals = new Complex[XL, YL];
        var loc = new Complex[XL, YL];

        for (int x = 0; x < XL; x++) for (int y = 0; y < YL; y++) {
            loc[x, y] = new Complex((x - XL / 2) / 256.0, (y - YL / 2) / 256.0);
            vals[x, y] = new Complex(0, 0);
        }

        for (int i = 0; i < ITER; i++) {
            for (int x = 0; x < XL; x++)
            for (int y = 0; y < YL; y++) {
                if (vals[x, y].Real > 4) continue;
                vals[x, y] = vals[x, y] * vals[x, y] + loc[x, y];
            }
        }
    }

}

public struct Complex {
    public double Real, Imaginary;
    public Complex(double a, double b) {
        Real = a;
        Imaginary = b;
    }
    public static Complex operator + (Complex a, Complex b) {
        return new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary);
    }
    public static Complex operator * (Complex a, Complex b) {
        return new Complex(a.Real*b.Real - a.Imaginary*b.Imaginary, a.Real*b.Imaginary + a.Imaginary*b.Real);
    }
}

}

编辑

GPU似乎是唯一可行的解决方案,我忽视了与C/C++的互操作性,因为我觉得加速不会太大以至于强迫我强制将来的插件进行互操作。

在研究了现有的GPU选项之后(我已经研究了一段时间了),我终于找到了我认为是一个很好的折衷方案。我选择了OpenCL,希望在我的程序发布时,大多数设备都能支持这个标准。OpenCLTemplate使用CLO提供一个易于理解的.NET(用于应用程序逻辑)和"OpenCLC99"(用于并行代码)之间的接口。插件可以包括用于硬件加速的OpenCL内核,以及用于System.Numerics.Complex的标准实现,以便于集成。

我希望随着处理器供应商采用OpenCLC99标准,有关编写OpenCLC99代码的可用教程数量会快速增长。这使我不需要在插件开发人员身上强制执行GPU编码,同时为他们提供了一种良好的语言,如果他们选择利用这个选项的话。这也意味着Ironpython脚本在编译时之前都不知道,尽管它们对GPU加速的访问是相同的,因为代码将直接通过opencl进行转换。

对于将来有兴趣将GPU加速与.NET项目集成的任何人,我强烈推荐OpenCLTemplate。opencl c99的学习开销是允许的。然而,它只比学习一个替代的API稍微困难一点,并且可能会得到示例和一般社区的更好支持。


我认为你最好的办法是把这些计算结果卸载到图形卡上。有OpenCL可以使用图形卡进行这类操作,也可以使用OpenGL明暗器。

要真正利用这一点,您需要并行计算。假设你想要平方根(我知道很简单,但原理是一样的)一百万个数字。在一个CPU上,您一次只能做一个,或者计算出您有多少个核心,合理地说是8个核心,并让每个核心对数据的一个子集执行计算。

例如,如果您将计算卸载到图形卡中,您将"馈送"数据,例如,一组140万个三维空间点(即每个顶点四个浮点),然后让顶点着色程序计算每个顶点的每个xyzw的平方根。一个图形卡有更多的内核,即使它只有100个,它仍然可以同时处理更多的数字,然后是一个CPU。

如果你愿意的话,我可以用更多的信息来充实这一点,尽管我不希望使用着色器,但是我需要用任何方法来处理它们。

编辑

看看这个相对便宜的卡,一个NvideaGT 220,你可以看到它有48个"CUDA"核心。这些是您在使用OpenCL和明暗器时使用的。

编辑2

好吧,看来你对使用GPU加速相当感兴趣。我无法帮助您使用OpenCL,从未研究过它,但我认为它与OpenGL/DirectX应用程序(使用着色程序,但不使用实际的图形应用程序)的工作原理大致相同。我将讨论DirectX的方式,因为这是我所知道的(只是关于),但从我的理解来看,OpenGL的方式或多或少是相同的。

首先,您需要创建一个窗口。当你想要跨平台的时候,GLUT可能是最好的方式,它不是世界上最好的图书馆,但是它给了你一扇漂亮而快速的窗户。由于您不会实际显示任何渲染,所以您可以将其设置为一个小窗口,其大小足以将标题设置为"硬件加速"。

一旦你的图形卡设置好,并准备好渲染的东西,你就可以通过以下教程进入这个阶段。这将使您进入创建3D模型并在屏幕上"动画化"它们的阶段。

接下来要创建一个顶点缓冲区,用输入数据填充该缓冲区。顶点通常是三(或四)个浮点数。如果你的价值观都是独立的,那就太酷了。但是如果你需要将它们组合在一起,比如说你实际上是在处理二维向量,那么你需要确保你正确地"打包"了数据。假设你想用二维向量做数学运算,而OpenGL使用的是三维向量,那么vector.x和vector.y就是你实际的输入向量和vector.z就是多余的数据。

你看,向量明暗器一次只能处理一个向量,它看不到多个向量作为输入,你可以考虑使用几何明暗器,它可以查看更大的数据集。

好的,设置一个顶点缓冲区,并将其弹出到图形卡上。你还需要写一个"顶点着色",这是一个文本文件,有一种类似C的语言,可以让你进行一些数学运算。它不是一个完整的C实现思想,但它看起来足够像C,您可以知道自己在做什么。OpenGL明暗器的精确输入和输出超出了我的理解范围,但是我确信一个简单的教程很容易找到。

有一件事是你自己要做的,那就是找出如何精确地将顶点着色的输出转到第二个缓冲区,这就是你的输出。顶点明暗器不会更改所设置缓冲区中的顶点数据,即常量(就明暗器而言),但可以将明暗器输出到第二个缓冲区。

你的计算结果是这样的

1
2
3
4
5
6
7
createvertexbuffer()
loadShader("path to shader code", vertexshader) // something like this I think
// begin 'rendering'
setShader(myvertexshader)
setvertexbuffer(myvertexbuffer)
drawpoints() // will now 'draw' your points
readoutputbuffer()

我希望这有帮助。就像我说的,我仍然在学习这个,甚至在那时我也在学习事物的DirectX方式。


使您的自定义结构可变,我获得30%。这减少了调用和内存使用

1
2
3
4
5
6
7
8
9
10
11
12
13
//instead of writing  (in TestCustomComplex())
vals[x, y] = vals[x, y] * vals[x, y] + loc[x, y];

//use
vals[x,y].MutableMultiAdd(loc[x,y]);

//defined in the struct as
public void MutableMultiAdd(Complex other)
    {
        var tempReal = (Real * Real - Imaginary * Imaginary) + other.Real;
        Imaginary =( Real * Imaginary + Imaginary * Real )+ other.Imaginary;
        Real = tempReal;
    }

对于矩阵乘法,还可以使用"unsafe fixed()"访问数组。使用这个,我获得了testcustomComplex()的15%。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static void TestCustomComplex()
    {
        var vals = new Complex[XL, YL];
        var loc = new Complex[XL, YL];

        for (int x = 0; x < XL; x++)
            for (int y = 0; y < YL; y++)
            {
                loc[x, y] = new Complex((x - XL / 2) / 256.0, (y - YL / 2) / 256.0);
                vals[x, y] = new Complex(0, 0);
            }

        unsafe
        {
            fixed (Complex* p = vals, l = loc)
            {
                for (int i = 0; i < ITER; i++)
                {
                    for (int z = 0; z < XL*YL; z++)
                    {
                        if (p[z].Real > 4) continue;
                        p[z] = p[z] * p[z] + l[z];
                    }
                }
            }
        }
    }


就个人而言,如果这是一个主要问题,我会创建一个C++ DLL,然后用它来做算术运算。您可以从C调用这个插件,这样您仍然可以利用WPF和反射等。

需要注意的一点是,调用插件并不是一个"快速"的过程,因此您希望确保一次性传递所有数据,而不是经常调用它。