【问题标题】:How to do Speedy Complex Arithmetic in C#如何在 C# 中进行快速复杂算术运算
【发布时间】:2011-02-16 00:01:30
【问题描述】:

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

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

我已经知道 C# 4.0 中的 Parallel.For;为了清楚起见,我的代码示例中省略了它。我也知道 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 稍微困难一些,并且可能会从示例和一般社区获得更好的支持。

【问题讨论】:

  • “我也知道 C# 通常不是高性能的好语言”——这是不正确的。
  • 您将无法加速单个复杂的加法/乘法。相反,您需要利用更大的计算,可能将一系列计算流水线化到 GPU
  • @Mitch 我认为他的意思是,你可以通过一些 C# 中没有的花哨的聪明来使 C++ 代码比 C# 更快。
  • “不正确”wisegeek.com/… 发表此类言论时,请提供链接或事实。
  • @cyberkiwi:大概你的意思是也适用于海报。此外,这是一个自愿网站;你去找链接怎么样?

标签: c#-4.0 performance fractals complex-numbers


【解决方案1】:

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

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

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

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

编辑

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

编辑 2

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

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

设置好显卡并准备好渲染内容后,请关注tutorials from here 进入此阶段。这将使您进入可以创建 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 的方式。

【讨论】:

  • 再一次,使用 CUDA 将我限制在 nvidea 上,而 OpenCL 则通过技术的新颖性限制了我。也就是说,我怎么能期望最终用户在他们的系统上安装有效的 OpenCL 驱动程序?我们如何使用 GPU 而不会遇到可移植性问题?
  • 不推荐使用 C/C++ 的真实答案?我为你感到骄傲。
  • 不要混用 openGL 和 openCL。两者都是跨平台,如果您将它们用于硬件加速计算,两者都会受到您机器中硬件的限制,如果您的 GPU 没有任何可编程着色器,那么您将无法进行硬件可编程着色器
  • 因此,如果 GPU 卸载是高容量数学的最佳答案(这是有道理的),那么将其集成到程序中的最简单方法是什么? GPU.Net 需要额外的构建步骤和许可证,因此无法用于第三方插件开发。 Brahma 似乎是一个不错的选择,但我找不到任何好的教程或最新的代码示例。看起来我会被绑定到 OpenCL dll 并要求插件包含 OpenCL 内核。有谁知道从 OpenCL 到 .Net 的其他任何好的、免费的集成?
【解决方案2】:

使您的自定义结构可变我获得了 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];
                    }
                }
            }
        }
    }

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

    • C++ 和 C# 之间的性能差异不太可能如此大,以至于无法通过从 DLL 调用方法和封送数据返回的开销来缓解来回。这是假设 C# 和其他 .NET 语言必须是“玩具”语言的典型错误答案之一,因为它们运行托管代码,因此像 C++ 这样的本机语言的速度必须突飞猛进。尽管这个故事对很多人来说似乎很有吸引力,但它也是错误的,并且不断地应用它会得到像这样的废话建议。
    猜你喜欢
    • 1970-01-01
    • 2021-05-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-17
    • 2011-05-07
    • 2018-11-06
    • 1970-01-01
    相关资源
    最近更新 更多