C++绘图Mandlebrot集,性能不好

C++ plotting Mandlebrot set, bad performance

本文关键字:性能 绘图 Mandlebrot C++      更新时间:2023-10-16

我不确定是否有实际的性能提升,或者我的电脑是不是又旧又慢,但我还是会问的。

因此,我尝试制作一个程序,使用cairo库绘制Mandelbrot集。

绘制像素的循环如下所示:

vector<point_t*>::iterator it;
for(unsigned int i = 0; i < iterations; i++){
  it = points->begin();
  //cout << points->size() << endl;
  double r,g,b;
  r = (double)i+1 / (double)iterations;
  g = 0;
  b = 0;
  while(it != points->end()){
    point_t *p = *it;
    p->Z = (p->Z * p->Z) + p->C;
    if(abs(p->Z) > 2.0){
      cairo_set_source_rgba(cr, r, g, b, 1);
      cairo_rectangle (cr, p->x, p->y, 1, 1);
      cairo_fill (cr);
      it = points->erase(it);
    } else {
      it++;
    }
  }
}

这个想法是给所有刚刚脱离集合的点涂上颜色,然后将它们从列表中删除,以避免再次评估它们。

它确实正确地渲染了集,但渲染所需的时间似乎比需要的要长得多。

有人能发现循环的任何性能问题吗?还是它已经很好了?

提前感谢:)

解决方案

非常好的答案,谢谢:)-我最终得到了一种混合答案。考虑到建议,我意识到计算每个点,将它们放在向量中,然后提取它们是对CPU时间和内存的巨大浪费。因此,该程序现在只计算每个点的Z值,即使使用point_t或向量也是如此。它现在跑得快多了!

编辑:我认为kuroi-neko的答案中的建议也是一个非常好的主意,如果你不关心"增量"计算,但有固定的迭代次数。

  1. 您应该使用vector<point_t>而不是vector<point_t*>

    CCD_ 3是指向CCD_ 4的指针的列表。每个点都存储在存储器中的某个随机位置。如果对这些点进行迭代,那么访问内存的模式看起来完全是随机的。你会得到很多缓存未命中。

    相反,vector<point_t>使用连续内存来存储点。因此,下一个点直接存储在当前点之后。这允许高效的缓存。

  2. 您不应该在内部循环中调用erase(it);

    每次对erase的调用都必须将所有元素移动到您删除的元素之后。这具有O(n)运行时。例如,您可以向point_t添加一个标志,指示不应再对其进行处理。在每次迭代后删除所有"非活动"点可能会更快。

  3. 使用cairo_rectangle绘制单个像素可能不是一个好主意。我建议您创建一个图像并存储每个像素的颜色。然后用一个draw调用绘制整个图像。

你的代码可能是这样的:

for(unsigned int i = 0; i < iterations; i++){
  double r,g,b;
  r = (double)i+1 / (double)iterations;
  g = 0;
  b = 0;
  for(vector<point_t>::iterator it=points->begin(); it!=points->end(); ++it) {
    point_t& p = *it;
    if(!p.active) {
      continue;
    }
    p.Z = (p.Z * p.Z) + p.C;
    if(abs(p.Z) > 2.0) {
      cairo_set_source_rgba(cr, r, g, b, 1);
      cairo_rectangle (cr, p.x, p.y, 1, 1);
      cairo_fill (cr);
      p.active = false;
    }
  }
  // perhaps remove all points where p.active = false
}

如果不能更改point_t,则可以使用额外的vector<char>来存储点是否已变为"非活动"。

Zn发散计算使算法速度变慢(当然,这取决于您正在处理的区域)。相比之下,像素绘制只是背景噪声。

您的循环存在缺陷,因为它使Zn计算速度变慢。

方法是在一个紧凑、优化的循环中计算每个点的散度,然后处理显示。

此外,永久存储Z是无用和浪费的
您只需要C作为输入,迭代次数作为输出。

假设你的points数组只包含C值(基本上你不需要所有这些向量垃圾,但它也不会影响性能),你可以这样做:

for(vector<point_t>::iterator it=points->begin(); it!=points->end(); ++it)
{
    point_t Z = 0;
    point_t C = *it;
    for(unsigned int i = 0; i < iterations; i++) // <-- this is the CPU burner
    {
        Z = Z * Z + C;
        if(abs(Z) > 2.0) break;
    }
    cairo_set_source_rgba(cr, (double)i+1 / (double)iterations, g, b, 1);
    cairo_rectangle (cr, p->x, p->y, 1, 1);
    cairo_fill (cr);
}

试着在有和没有cairo的情况下运行它,你应该看不到执行时间的明显差异(除非你看到的是集合的一个空位)。

现在,如果你想走得更快,试着将Z=Z*Z+C计算分解为实部和虚部并进行优化。你甚至可以使用mmx或其他方法进行并行计算。

当然,获得另一个重要速度因素的方法是在可用的CPU核心上并行化算法(即,将显示区域划分为子集,并让不同的工作线程并行计算这些部分)。

不过,这并不像看上去那么明显,因为每个子图片都有不同的计算时间(黑色区域的计算速度非常慢,而白色区域几乎是即时计算的)
一种方法是分割大量矩形区域,并让所有工作线程从公共池中随机选择一个矩形,直到所有矩形都得到处理
这个简单的负载平衡方案可以确保当它的伙伴在显示器的其他部分忙碌时,没有CPU核心空闲。

优化性能的第一步是找出慢的地方。您的代码混合了三个任务——迭代以计算点是否逃逸,操纵要测试的点向量,以及绘制点。

将这三项行动分开,并衡量它们的贡献。您可以通过使用simd运算将转义计算并行化来优化转义计算。如果你想删除矢量,你可以通过不从矢量中擦除来优化矢量操作,但如果你想保留它,你可以将它添加到另一个矢量中(因为擦除是O(N)和加法O(1)),并通过使用点的矢量而不是指向点的指针来提高局部性,如果绘图速度较慢,则使用屏幕外位图,并通过操作后备内存设置点,而不是使用cairo函数。

(我本来打算发布这篇文章,但@Werner Henze已经在评论中提出了同样的观点,因此社区wiki)