How to do Speedy Complex Arithmetic in C#
我现在正在研究一个C分形生成器项目,它需要很多复杂数字的算术运算,我正在努力思考加速数学运算的方法。下面是一组简化的代码,使用三种数据存储方法之一测试Mandelbrot计算的速度,如
基本上,我看到使用
我已经知道C 4.0中的
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和反射等。
需要注意的一点是,调用插件并不是一个"快速"的过程,因此您希望确保一次性传递所有数据,而不是经常调用它。