0

我现在正在处理一个 C# Fractal Generator 项目,该项目需要大量的复数算术,我正在尝试想办法加快数学运算速度。下面是一组简化的代码,它使用三种数据存储方法中的一种来测试 Mandelbrot 计算的速度,如TestNumericsComplexTestCustomComplex和所示TestPairedDoubles。请理解 Mandelbrot 只是一个示例,我打算让未来的开发人员能够创建插件分形公式。

基本上我认为 usingSystem.Numerics.Complex是一个不错的想法,而使用一对双打或自定义 Complex 结构是可以接受的想法。我可以使用 gpu 执行算术,但这不会限制或破坏可移植性吗?我尝试改变内部循环的顺序(i,x,y)无济于事。我还能做些什么来帮助加快内部循环?我遇到页面错误问题了吗?与浮点值相比,使用定点数系统会提高我的速度吗?

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

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使用cloo在 .Net(用于应用程序逻辑)和“OpenCL C99”(用于并行代码)之间提供易于理解的接口。插件可以包括用于硬件加速的 OpenCL 内核以及带有 System.Numerics.Complex 的标准实现,以便于集成。

随着标准被处理器供应商采用,我预计有关编写 OpenCL C99 代码的可用教程的数量会迅速增长。这使我无需对插件开发人员强制执行 GPU 编码,同时为他们提供精心设计的语言(如果他们选择利用该选项)。这也意味着 IronPython 脚本将具有同等的 GPU 加速访问权限,尽管直到编译时才知道,因为代码将直接通过 OpenCL 进行转换。

对于将来有兴趣将 GPU 加速与 .Net 项目集成的任何人,我强烈推荐 OpenCLTemplate。学习 OpenCL C99 有一定的开销。但是,它仅比学习替代 API 稍微困难一些,并且可能会从示例和一般社区获得更好的支持。

4

3 回答 3

2

我认为您最好的选择是将这些计算加载到显卡上。有 openCL 可以使用显卡来处理这类事情,也可以使用 openGL 着色器。

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

例如,如果你将计算卸载到显卡上,你会“输入”你的数据,比如空间中的 1/4 百万个 3D 点(即每个顶点四个浮点数),然后让顶点着色器计算正方形每个顶点的每个 xyzw 的根。显卡有更多的内核,即使它只有 100 个,它仍然可以同时处理比 CPU 更多的数字。

如果您愿意,我可以用更多信息来充实这一点,尽管我不希望使用着色器,但我需要以任何方式开始使用它们。

编辑

看看这张相对便宜的显卡和 nvidea GT 220,你可以看到它有 48 个“CUDA”内核。这些是您在使用诸如 openCL 和着色器之类的东西时所使用的。

编辑 2

好的,所以您似乎对使用 GPU 加速很感兴趣。我无法帮助您使用 openCL,从未研究过它,但我认为它可以与使用着色器但没有实际图形应用程序的 openGL/DirectX 应用程序大致相同。我将谈论 DirectX 的方式,因为这是我所知道的(只是),但据我了解,对于 openGL,它或多或少都是相同的。

首先,您需要创建一个窗口。当您想要跨平台时,GLUT 可能是最好的方法,它不是世界上最好的库,但它为您提供了一个又好又快的窗口。由于您实际上不会显示任何渲染,因此您可以将其设置为一个小窗口,大到足以将他的标题设置为“硬件加速”之类的东西。

设置好显卡并准备好渲染内容后,您可以按照此处的教程进入此阶段。这将使您进入可以创建 3D 模型并在屏幕上“动画化”它们的阶段。

接下来,您要创建一个用输入数据填充的顶点缓冲区。一个顶点通常是三个(或四个)浮点数。如果你的价值观都是独立的,那很酷。但是如果您需要将它们组合在一起,比如说您实际上正在使用 2D 向量,那么您需要确保正确“打包”数据。假设您想使用 2D 向量进行数学运算,而 openGL 正在使用 3D 向量,那么 vector.x 和 vector.y 是您的实际输入向量,而 vector.z 只是备用数据。

你看,矢量着色器一次只能处理一个矢量,它不能看到超过一个矢量作为输入,你可以考虑使用可以查看更大数据集的几何着色器。

没错,您设置了一个顶点缓冲区并将其弹出到显卡上。您还需要编写一个“顶点着色器”,这是一个文本文件,具有一种类似于 C 的语言,可以让您执行一些数学运算。它不是一个完整的 C 实现思想,但它看起来很像 C,让您知道自己在做什么。我无法详细了解 openGL 着色器的来龙去脉,但我确信一个简单的教程很容易找到。

您自己需要做的一件事是找出如何准确地将顶点着色器的输出转到第二个缓冲区,这实际上是您的输出。顶点着色器不会更改您设置的缓冲区中的顶点数据,这是恒定的(就着色器而言),但您可以让着色器输出到第二个缓冲区。

你的计算看起来像这样

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 的方式。

于 2011-02-16T00:25:14.443 回答
0

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

//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%。

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];
                    }
                }
            }
        }
    }
于 2012-06-19T15:03:05.530 回答
-1

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

需要注意的一点是,调用插件并不完全是“快速”,因此您要确保一次性传递所有数据而不是经常调用它。

于 2011-02-16T00:07:09.553 回答