使C#mandelbrot绘图更有效率

时间:2013-07-01 14:07:28

标签: c# optimization gpu fractals mandelbrot

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

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

我的问题是,它很慢。如果没有深度变焦,它可以很好地工作并且移动并且变焦非常平滑,每次绘制只需不到一秒钟,但是一旦我开始放大一点并到达需要更多计算的地方,它就变得非常慢。 / p>

在其他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?它甚至是那么简单还是我需要学习很多东西?)。如果有人可以给我发送关于这个主题的正确教程的链接,或者告诉我一般情况下这将是很好的。

  3. 非常感谢您阅读这篇文章,并希望您能帮助我:)。

3 个答案:

答案 0 :(得分:4)

如果您决定将处理移至gpu,则可以从多个选项中进行选择。由于您使用的是C#,XNA将允许您使用HLSL。如果您选择此选项,RB Whitaker拥有最简单的XNA教程。另一种选择是OpenCLOpenTK附带了julia set fractal的演示程序。这将非常简单地修改以显示mandlebrot集。见here 请记住找到与源代码一起使用的GLSL着色器。

  

关于GPU,示例对我没用,因为我绝对有   不知道这个话题,它是如何工作的,是什么样的   GPU可以做的计算(或者甚至如何访问?)

然而,不同的GPU软件工作方式不同......

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

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

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

pipeline

顶点着色器负责在3D空间中获取点并计算它们在2D视场中的位置。 (因为你在2D工作,对你来说不是很重要)

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

OpenCL是一个不同的故事,它面向通用GPU计算(即:不仅仅是图形)。它功能更强大,可用于GPU,DSP和构建超级计算机。

答案 1 :(得分:3)

GPU的WRT编码,你可以看看Cudafy.Net(它也是OpenCL,它与NVidia无关)开始了解正在发生的事情,甚至可能做你需要的一切。我很快发现它 - 我的显卡 - 不适合我的需要,但对于你所处的舞台上的Mandelbrot,应该没问题。

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

当我自己进行分形渲染时,由于已经概述的原因,我避免绘制到位图并延迟渲染阶段。除此之外,我倾向于编写大量多线程代码,这对于尝试访问位图非常不利。相反,我写了一个公共存储 - 最近我使用了MemoryMappedFile(内置.Net类),因为这给了我相当不错的随机访问速度和巨大的可寻址区域。我也倾向于将我的结果写入队列并让另一个线程处理将数据提交到存储器;每个Mandelbrot像素的计算时间将“粗糙” - 也就是说它们不会总是占用相同的时间长度。因此,您的像素提交可能是非常低迭代次数的瓶颈。将其转移到另一个线程意味着您的计算线程永远不会等待存储完成。

我目前正在使用Mandelbrot集的Buddhabrot可视化,看着使用GPU来扩展渲染(因为它需要花费很长时间的CPU)并且具有巨大的结果集。我正在考虑定位一个8千兆像素的图像,但我已经意识到我需要偏离像素的约束,并且可能由于精度问题而远离浮点运算。我也将不得不购买一些新的硬件,这样我就可以与GPU进行不同的交互 - 不同的计算工作将在不同的时间完成(根据我先前的迭代计数评论),所以我不能只是批量线程并等待他们所有人都可以完成,而不会浪费大量时间等待整个批次中特别高的迭代次数。

另一点让我几乎没有看到关于Mandelbrot Set的观点是它是对称的。你可能需要做两倍的计算。

答案 2 :(得分:1)

要将处理转移到GPU,您可以在此处获得许多优秀示例:

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

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

我不是分形专家,但你似乎已经对优化做了很多。超出这个范围可能会使代码更难以阅读和维护,所以你应该问自己这是值得的。

我在其他分形程序中经常观察到的一种技术是:在缩放时,以较低分辨率计算分形并在渲染过程中将其拉伸至完整大小。变焦停止后立即以全分辨率渲染。

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