让 C# mandelbrot 绘图更高效
Posted
技术标签:
【中文标题】让 C# mandelbrot 绘图更高效【英文标题】:Making C# mandelbrot drawing more efficient 【发布时间】:2013-06-28 16:37:05 【问题描述】:首先,我知道这个问题听起来确实好像我没有搜索过,但我确实搜索过很多次。
我为 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;
我可以做些什么来改善这一点?您在我的代码中发现任何明显的优化问题吗?
目前我知道有两种方法可以改进它:
我需要对数字使用不同的类型,double 的准确性受到限制,我确信有更好的非内置替代类型更快(它们相乘和相加更快)并且具有更高的准确性,我只需要有人指出我需要查看的地方并告诉我这是不是真的。
我可以将处理转移到 GPU。我不知道如何做到这一点(也许是OpenGL?DirectX?它甚至那么简单还是我需要学习很多东西?)。如果有人可以向我发送有关此主题的适当教程的链接,或者大致告诉我,那就太好了。
非常感谢您阅读这么远,希望您能帮助我:)
【问题讨论】:
float 通常更快,尽管我认为这取决于您使用的处理器。如果使用 gpu,float 通常比 double 快。 【参考方案1】:如果您决定将处理转移到 gpu,您可以从多个选项中进行选择。由于您使用的是 C#,XNA 将允许您使用 HLSL。如果您选择此选项,RB Whitaker 拥有最简单的 XNA 教程。另一种选择是OpenCL。 OpenTK 附带一个 julia set 分形的演示程序。这将很容易修改以显示 mandlebrot 集。见here 只要记住找到源代码附带的 GLSL 着色器即可。
关于 GPU,示例对我没有帮助,因为我绝对有 不知道这个话题,它是如何工作的以及是什么样的 GPU 可以做的计算(或者它是如何访问的?)
但是不同的 GPU 软件工作方式不同......
通常,程序员会使用 HLSL、GLSL 或 OpenCL 等着色器语言为 GPU 编写程序。用 C# 编写的程序会加载着色器代码并对其进行编译,然后使用 API 中的函数将作业发送到 GPU,然后再将结果返回。
查看FX Composer 或渲染猴子,如果您想练习着色器而不必担心 API。
如果您使用的是 HLSL,则渲染管道如下所示。
顶点着色器负责在 3D 空间中获取点并计算它们在 2D 视野中的位置。 (因为您在 2D 中工作,所以对您来说不是什么大问题)
像素着色器负责在顶点着色器完成后对像素应用着色器效果。
OpenCL 是另一回事,它面向通用 GPU 计算(即:不仅仅是图形)。它更强大,可用于 GPU、DSP 和构建超级计算机。
【讨论】:
【参考方案2】:GPU 的 WRT 编码,您可以查看 Cudafy.Net(它也支持 OpenCL,它与 NVidia 无关)开始了解正在发生的事情,甚至可以在那里完成您需要的一切。我很快发现它 - 以及我的显卡 - 不适合我的需求,但对于你所处阶段的 Mandelbrot 来说,应该没问题。
简而言之:您使用 C 风格(通常是 Cuda C 或 OpenCL)为 GPU 编写代码,然后将“内核”(您编译的 C 方法)推送到 GPU,然后是任何源数据,然后调用该“内核” ",通常带有参数来说明要使用什么数据 - 或者可能是一些参数来告诉它将结果放在内存中的哪个位置。
当我自己进行分形渲染时,我避免绘制位图,原因已经概述并推迟了渲染阶段。除此之外,我倾向于编写大量多线程代码,这对于尝试访问位图非常不利。相反,我写到一个普通的商店——最近我使用了一个 MemoryMappedFile(一个内置的 .Net 类),因为它给了我相当不错的随机访问速度和巨大的可寻址区域。我也倾向于将我的结果写入队列并让另一个线程处理将数据提交到存储;每个 Mandelbrot 像素的计算时间将“参差不齐”——也就是说,它们不会总是花费相同的时间长度。因此,您的像素提交可能是非常低的迭代次数的瓶颈。将其分配给另一个线程意味着您的计算线程永远不会等待存储完成。
我目前正在使用 Mandelbrot 集的 Buddhabrot 可视化,正在研究使用 GPU 来扩展渲染(因为它需要很长时间使用 CPU)并获得巨大的结果集。我正在考虑以 8 GB 的图像为目标,但我已经意识到我需要偏离像素的约束,并且可能由于精度问题而远离浮点运算。我还必须购买一些新硬件,这样我就可以与 GPU 进行不同的交互——不同的计算作业将在不同的时间完成(根据我之前的迭代计数评论),所以我不能只触发一批线程并等待让它们全部完成,而不会浪费大量时间等待整个批次中的一个特别高的迭代次数。
关于曼德布洛特集,我几乎从未见过有人提出的另一点是它是对称的。您可能需要进行两倍的计算。
【讨论】:
kluge.in-chemnitz.de/documents/fractal/node9.html 答案就在那里 :) 混沌并不意味着随机,Mandelbrot 集合具有高度的可预测性。【参考方案3】:为了将处理转移到 GPU,这里有很多很好的例子:
https://www.shadertoy.com/results?query=mandelbrot
请注意,您需要支持 WebGL 的浏览器才能查看该链接。在 Chrome 中效果最佳。
我不是分形方面的专家,但您似乎已经在优化方面取得了长足的进步。超出此范围可能会使代码更难阅读和维护,因此您应该问问自己这样做是否值得。
我在其他分形程序中经常观察到的一种技术是:在缩放时,以较低的分辨率计算分形,并在渲染期间将其拉伸到全尺寸。然后在缩放停止后立即以全分辨率渲染。
另一个建议是,当您使用多个线程时,您应该注意每个线程不要读/写其他线程的内存,因为这会导致缓存冲突并损害性能。一种好的算法可以将工作拆分为扫描线(而不是像现在这样四分之二)。创建多个线程,然后只要剩下要处理的行,就将扫描线分配给可用的线程。让每个线程将像素数据写入本地内存,并在每行之后将其复制回主位图(以避免缓存冲突)。
【讨论】:
非常感谢您抽出宝贵时间回答 :) 关于 GPU,示例对我没有帮助,因为我完全不知道这个主题,它是如何工作的以及什么样的计算GPU 可以做到(或者它是如何访问的?)。我希望首先获得一些基本信息。关于进一步的优化,我不介意代码的可读性。低分辨率缩放是我考虑过的,但我希望我可以先做其他事情。 关于缓存冲突:我不太明白,为什么会有缓存冲突?如果我确保每个线程都准确地写入内存,它应该仍然存在缓存冲突吗?为什么扫描线是更好的选择(它们不只是分割图像的另一种方式吗?) @Omer 扫描线很好,因为它们是内存中的一个连续块,这对 CPU 缓存也有好处。最好在连续内存中写入(这就是为什么以 y/x 顺序而不是 x/y 遍历像素更好的原因)。发生冲突是因为缓存重叠,多个线程可以在缓存中拥有相同的 4096(比如说)字节内存,因此即使它们写入该内存的不同部分,它们也会发生冲突。以上是关于让 C# mandelbrot 绘图更高效的主要内容,如果未能解决你的问题,请参考以下文章
C#不要再使用Npoi啦,使用MiniExcel操作Excel文件更快更高效!