9

首先,我知道这个问题听起来真的好像我没有搜索,但我做了很多。

我为 C# 编写了一个小的 Mandelbrot 绘图代码,它基本上是一个带有 PictureBox 的 Windows 窗体,我在其上绘制了 Mandelbrot 集。

我的问题是,它很慢。如果没有深度缩放,它会做得很好,并且移动和缩放非常流畅,每张图只需不到一秒钟的时间,但是一旦我开始放大一点并到达需要更多计算的地方,它就会变得非常慢。

在其他 Mandelbrot 应用程序中,我的计算机在我的应用程序中运行速度慢得多的地方运行得非常好,所以我猜我可以做很多事情来提高速度。

我做了以下事情来优化它:

  • 我没有在位图对象上使用 SetPixel GetPixel 方法,而是使用 LockBits 方法直接写入内存,这使事情变得更快。

  • 我没有使用复数对象(使用我自己创建的类,而不是内置类),而是使用 2 个变量 re 和 im 来模拟复数。这样做让我减少了乘法,因为对实部和虚部求平方是在计算过程中完成的几次,所以我只是将平方保存在一个变量中并重用结果而无需重新计算它。

  • 我使用 4 个线程来绘制 Mandelbrot,每个线程执行图像的不同四分之一,并且它们都同时工作。据我了解,这意味着我的 CPU 将使用其 4 个内核来绘制图像。

  • 我使用 Escape Time 算法,据我所知,这是最快的?

这是我在像素之间移动和计算的方式,它被注释掉了,所以我希望它是可以理解的:

        //Pixel by pixel loop:
        for (int r = rRes; r < wTo; r++)
        {
            for (int i = iRes; i < hTo; i++)
            {

                //These calculations are to determine what complex number corresponds to the (r,i) pixel.
                double re = (r - (w/2))*step + zeroX ;
                double im = (i - (h/2))*step - zeroY;

                //Create the Z complex number
                double zRe = 0;
                double zIm = 0;

                //Variables to store the squares of the real and imaginary part.
                double multZre = 0;
                double multZim = 0;

                //Start iterating the with the complex number to determine it's escape time (mandelValue)
                int mandelValue = 0;
                while (multZre + multZim < 4 && mandelValue < iters)
                {
                    /*The new real part equals re(z)^2 - im(z)^2 + re(c), we store it in a temp variable
                    tempRe because we still need re(z) in the next calculation
                        */
                    double tempRe = multZre - multZim + re; 

                    /*The new imaginary part is equal to 2*re(z)*im(z) + im(c)
                        * Instead of multiplying these by 2 I add re(z) to itself and then multiply by im(z), which
                        * means I just do 1 multiplication instead of 2.
                        */
                    zRe += zRe; 
                    zIm = zRe * zIm + im;

                    zRe = tempRe; // We can now put the temp value in its place.

                    // Do the squaring now, they will be used in the next calculation.
                    multZre = zRe * zRe; 
                    multZim = zIm * zIm; 

                    //Increase the mandelValue by one, because the iteration is now finished.
                    mandelValue += 1;
                }


                //After the mandelValue is found, this colors its pixel accordingly (unsafe code, accesses memory directly):
                //(Unimportant for my question, I doubt the problem is with this because my code becomes really slow
                //    as the number of ITERATIONS grow, this only executes more as the number of pixels grow).
                Byte* pos = px + (i * str) + (pixelSize * r);
                byte col = (byte)((1 - ((double)mandelValue / iters)) * 255);
                pos[0] = col;
                pos[1] = col;
                pos[2] = col;

            }
        }

我能做些什么来改善这一点?您在我的代码中发现任何明显的优化问题吗?

现在有两种方法我知道我可以改进它:

  1. 我需要对数字使用不同的类型,double 的准确性受到限制,我确信有更好的非内置替代类型更快(它们乘法和加法更快)并且具有更高的准确性,我只需要有人来指出我需要看的地方并告诉我这是不是真的。

  2. 我可以将处理转移到 GPU。我不知道如何做到这一点(也许是OpenGL?DirectX?它甚至那么简单还是我需要学习很多东西?)。如果有人可以向我发送有关此主题的适当教程的链接,或者总体上告诉我,那就太好了。

非常感谢您阅读这么远,希望您能帮助我:)

4

3 回答 3

5

如果您决定将处理转移到 gpu,您可以从多个选项中进行选择。由于您使用的是 C#,XNA 将允许您使用 HLSL。如果您选择此选项,RB Whitaker拥有最简单的 XNA 教程。另一种选择是OpenCLOpenTK带有一个 julia set 分形的演示程序。这将很容易修改以显示 mandlebrot 集。见这里 只要记住找到源代码附带的 GLSL 着色器。

关于 GPU,例子对我没有帮助,因为我完全不知道这个话题,它是如何工作的,以及 GPU 可以做什么样的计算(或者它是如何访问的?)

不同的 GPU 软件的工作方式不同,但是......

通常,程序员会使用 HLSL、GLSL 或 OpenCL 等着色器语言为 GPU 编写程序。用 C# 编写的程序将加载着色器代码并对其进行编译,然后使用 API 中的函数将作业发送到 GPU,然后再将结果返回。

如果您想练习着色器而不必担心 API,请查看FX Composer或渲染猴子。

如果您使用的是 HLSL,则渲染管道如下所示。

管道

顶点着色器负责在 3D 空间中获取点并计算它们在 2D 视野中的位置。(因为您在 2D 中工作,所以对您来说不是一个大问题)

像素着色器负责在顶点着色器完成后将着色器效果应用于像素。

OpenCL 是另一回事,它面向通用 GPU 计算(即:不仅仅是图形)。它更强大,可用于 GPU、DSP 和构建超级计算机。

于 2013-08-13T07:18:57.663 回答
3

GPU 的 WRT 编码,您可以查看 Cudafy.Net(它也包含 OpenCL,它与 NVidia 无关)开始了解正在发生的事情,甚至可能在那里完成您需要的一切。我很快发现它 - 以及我的显卡 - 不适合我的需求,但对于你所处阶段的 Mandelbrot,它应该没问题。

简而言之:您使用 C 风格(通常是 Cuda C 或 OpenCL)为 GPU 编写代码,然后将“内核”(您编译的 C 方法)推送到 GPU,然后是任何源数据,然后经常调用该“内核”使用参数来说明要使用的数据 - 或者可能是一些参数来告诉它将结果放在内存中的哪个位置。

当我自己进行分形渲染时,我避免绘制位图,原因已经概述并推迟了渲染阶段。除此之外,我倾向于编写大量多线程代码,这对于尝试访问位图非常不利。取而代之的是,我写到一个普通的商店——最近我使用了一个 MemoryMappedFile(一个内置的 .Net 类),因为它给了我相当不错的随机访问速度和巨大的可寻址区域。我也倾向于将我的结果写入一个队列,并让另一个线程处理将数据提交到存储;每个 Mandelbrot 像素的计算时间将“参差不齐”——也就是说,它们不会总是花费相同的时间长度。因此,您的像素提交可能是非常低的迭代次数的瓶颈。

我目前正在使用 Mandelbrot 集的 Buddhabrot 可视化,研究使用 GPU 来扩展渲染(因为它需要很长时间使用 CPU)并获得巨大的结果集。我正在考虑以 8 GB 的图像为目标,但我已经意识到我需要偏离像素的约束,并且可能由于精度问题而远离浮点运算。我还必须购买一些新硬件,这样我就可以与 GPU 进行不同的交互——不同的计算作业将在不同的时间完成(根据我之前的迭代计数评论),所以我不能只触发一批线程并等待让它们全部完成,而不会浪费大量时间等待整个批次中的一个特别高的迭代计数。

我几乎从未见过关于曼德布洛特集的另一点是它是对称的。您可能需要进行两倍的计算。

于 2013-08-09T13:22:43.720 回答
1

为了将处理转移到 GPU,这里有很多很好的例子:

https://www.shadertoy.com/results?query=mandelbrot

请注意,您需要支持 WebGL 的浏览器才能查看该链接。在 Chrome 中效果最好。

我不是分形方面的专家,但您似乎已经在优化方面取得了长足的进步。超出此范围可能会使代码更难阅读和维护,因此您应该问问自己这是值得的。

我在其他分形程序中经常观察到的一种技术是:在缩放时,以较低的分辨率计算分形,并在渲染期间将其拉伸到全尺寸。然后在缩放停止后立即以全分辨率渲染。

另一个建议是,当您使用多个线程时,您应该注意每个线程不要读/写其他线程的内存,因为这会导致缓存冲突并损害性能。一种好的算法可以将工作拆分为扫描线(而不是像现在这样四分之二)。创建多个线程,然后只要剩下要处理的行,就将扫描线分配给可用的线程。让每个线程将像素数据写入本地内存,并在每行之后将其复制回主位图(以避免缓存冲突)。

于 2013-07-01T17:13:59.327 回答