这是一个C ++程序,它在图形窗口中输出Mandelbrot分形图像,并允许用户缩放和平移图像,每次用户这样做都会生成新图像。我在图形部分使用了SFML。

因为每次缩放或平移后都会生成新图像,所以它非常慢,特别是对于1000x600图像(它可能需要半秒钟新图像加载)。

#include <SFML/Graphics.hpp>
#include <cmath>

const int IMAGE_WIDTH = 1000;
const int IMAGE_HEIGHT = 600;
double zoom = 0.004; // allow the user to zoom in and out...
double offsetX = -0.7; // and move around
double offsetY = 0.0;
const int MAX = 127; // maximum number of iterations for mandelbrot()
                     // don't increase MAX or the colouring will look strange

int mandelbrot(double, double, int);
sf::Color getColor(int);

int main() {
    sf::RenderWindow window(sf::VideoMode(IMAGE_WIDTH, IMAGE_HEIGHT), "Mandelbrot");
    window.setFramerateLimit(30);

    sf::Image image;
    image.create(IMAGE_WIDTH, IMAGE_HEIGHT, sf::Color(0, 0, 0));
    sf::Texture texture;
    sf::Sprite sprite;

    bool stateChanged = true; // track whether the image needs to be regenerated

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            switch (event.type) {
                case sf::Event::Closed:
                    window.close();
                    break;
                case sf::Event::KeyPressed:
                    stateChanged = true; // image needs to be recreated when the user changes zoom or offset
                    switch (event.key.code) {
                        case sf::Keyboard::Escape:
                            window.close();
                            break;
                        case sf::Keyboard::Equal:
                            zoom *= 0.9;
                            break;
                        case sf::Keyboard::Dash:
                            zoom /= 0.9;
                            break;
                        case sf::Keyboard::W:
                            offsetY -= 40 * zoom;
                            break;
                        case sf::Keyboard::S:
                            offsetY += 40 * zoom;
                            break;
                        case sf::Keyboard::A:
                            offsetX -= 40 * zoom;
                            break;
                        case sf::Keyboard::D:
                            offsetX += 40 * zoom;
                            break;
                        default: break;
                    }
                default:
                    break;
            }
        }

        if (stateChanged) { // only generate a new image if something has changed, to avoid unnecessary lag
            for (int x = 0; x < IMAGE_WIDTH; x++) {
                for (int y = 0; y < IMAGE_HEIGHT; y++) {
                    // convert x and y to the appropriate complex number
                    double real = (x - IMAGE_WIDTH / 2.0) * zoom + offsetX;
                    double imag = (y - IMAGE_HEIGHT / 2.0) * zoom + offsetY;
                    int value = mandelbrot(real, imag, MAX);
                    image.setPixel(x, y, getColor(value));
                }
            }
            texture.loadFromImage(image);
            sprite.setTexture(texture);
        }

        window.clear();
        window.draw(sprite);
        window.display();

        stateChanged = false;
    }

    return 0;
}

int mandelbrot(double startReal, double startImag, int maximum) {
    int counter = 0;
    double zReal = startReal;
    double zImag = startImag;
    double nextRe;

    while (pow(zReal, 2.0) + pow(zImag, 2.0) <= 4.0 && counter <= maximum) {
        nextRe = pow(zReal, 2.0) - pow(zImag, 2.0) + startReal;
        zImag = 2.0 * zReal * zImag + startImag;
        zReal = nextRe;
        if (zReal == startReal && zImag == startImag) { // a repetition indicates that the point is in the Mandelbrot set
            return -1; // points in the Mandelbrot set are represented by a return value of -1
        }
        counter += 1;
    }

    if (counter >= maximum) {
        return -1; // -1 is used here to indicate that the point lies within the Mandelbrot set
    } else {
        return counter; // returning the number of iterations allows for colouring
    }
}

sf::Color getColor(int iterations) {
    int r, g, b;

    if (iterations == -1) {
        r = 0;
        g = 0;
        b = 0;
    } else if (iterations == 0) {
        r = 255;
        g = 0;
        b = 0;
    } else {
        // colour gradient:      Red -> Blue -> Green -> Red -> Black
        // corresponding values:  0  ->  16  ->  32   -> 64  ->  127 (or -1)
        if (iterations < 16) {
            r = 16 * (16 - iterations);
            g = 0;
            b = 16 * iterations - 1;
        } else if (iterations < 32) {
            r = 0;
            g = 16 * (iterations - 16);
            b = 16 * (32 - iterations) - 1;
        } else if (iterations < 64) {
            r = 8 * (iterations - 32);
            g = 8 * (64 - iterations) - 1;
            b = 0;
        } else { // range is 64 - 127
            r = 255 - (iterations - 64) * 4;
            g = 0;
            b = 0;
        }
    }

    return sf::Color(r, g, b);
}


打开后的窗口立即:放大(使用+键)并向上平移(使用W键)后的窗口:



评论

您的审查目标是什么?速度?可读性好吗?

@没人,我对Java和Python更加熟悉,所以我有兴趣使我的代码与C ++风格更加一致。可读性也是我关注的问题。对我而言,速度并不是优先考虑的事项,因为我认为我对改进的地方有一个大概的认识。
您在执行window.close()之后调用window.clear(),window.draw()和window.display(),您真的要这样做吗?

o_O花费了整整5分钟的时间才半秒回想。我们取得了多少进展。

还可以选择保存图像缓冲区然后替换整个图像的方法,而不是按像素操作和调用函数吗?

#1 楼

我看到了一些可以帮助您改进代码的东西。这是许多编译器可以自己进行的优化,但是有助于显式地编写它。特别是,在main内的嵌套循环将重新计算图像。现在,循环看起来像这样:
for (int x = 0; x < IMAGE_WIDTH; x++) {
    for (int y = 0; y < IMAGE_HEIGHT; y++) {
        // convert x and y to the appropriate complex number
        double real = (x - IMAGE_WIDTH / 2.0) * zoom + offsetX;
        double imag = (y - IMAGE_HEIGHT / 2.0) * zoom + offsetY;
        int value = mandelbrot(real, imag, MAX);
        image.setPixel(x, y, getColor(value));
    }
}


由于仅realimag在循环内发生了变化,因此用以下所有内容替换实际上很简单:
br避免特殊情况

x的值用于表示Mandelbrot集中的点,但是在分配颜色时会显式检查此值,该值等效于y。为什么不简单地分配-1的值并避免特殊情况?

更喜欢简单的查找代码

MAX函数获取0到MAX(如果采用了先前的建议)范围内的单个数字并返回颜色。为什么不用表查找替换它呢?一次(在编译时或在运行时)计算表值,然后使用该表。在MAX的开头可以有以下内容:

double real = x * zoom - IMAGE_WIDTH / 2.0 * zoom + offsetX;
double imag = y * zoom - IMAGE_HEIGHT / 2.0 * zoom + offsetY;


然后循环将使用以下代码: >不要对浮点数使用getColor()

main与浮点数一起使用通常不是一个好主意,因为如果它们仅相差很小(例如1e-99),则它们不相等。实际上,如果您修改代码以计算该语句计算为==的次数,我想您会发现它永远不会这样做,因此只是在浪费时间。

有关浮点问题的更多信息,我推荐David Goldberg撰写的精彩文章“每个计算机科学家都应了解的浮点算术知识”。 br /> ==循环可以像这样更简单地重写:首先,由于原始的第三个参数true始终以mandelbrot的值传递,因此只需将其从参数列表中删除,然后将maximum的值直接替换即可。而不是使结构更明显的MAX循环。

接下来,对MAXfor值进行了预先计算,使代码更短,更简单(并且可能更有效)。

最后,早期的救助是在循环内完成的。只有在循环结束时,才返回值while

不需要省略工作

由于在下一行无论如何都会写入整个窗口。只需消除该行即可。

消除全局变量

r2之外的所有变量都可以在i2内设为局部。

消除帧率限制

除非有理由这样做,否则您可能会发现消除对MAX的调用或将其值设置为window.clear()可使程序响应更快。应用完上述所有建议后,我发现帧速率限制是阻止机器上速度进一步提高的原因。
如果不需要,请不要更新屏幕

现在,任何键(即使是无效键,也会导致屏幕重新计算。可以通过在键事件处理main语句的默认情况下添加MAX来轻松解决此问题。

使用C + +11个线程

可以毫不费力地并行化此代码。我已经更改了main的结尾,因此现在看起来像这样:

double real = 0 * zoom - IMAGE_WIDTH / 2.0 * zoom + offsetX;
double imagstart = 0 * zoom - IMAGE_HEIGHT / 2.0 * zoom + offsetY;
for (int x = 0; x < IMAGE_WIDTH; x++, real += zoom) {
    double imag = imagstart;
    for (int y = 0; y < IMAGE_HEIGHT; y++, imag += zoom) {
        // convert x and y to the appropriate complex number
        int value = mandelbrot(real, imag, MAX);
        image.setPixel(x, y, getColor(value));
    }
}


如您所见,嵌套循环已被调用main代替。现在看起来是这样的:
有了这一点,在我达到2e-16或更小的变焦倍数之前,一切都会变得更快,看起来也更好,而由于前面提到的浮点问题,出现明显的条纹。更仔细地重新计算可能会消除或推迟计算,但我没有花时间这样做。处理共享线程的方法,因为它至少会导致数据争用和损坏的可能性。但是,正如我猜想的那样,window.setFramerateLimit对象的结构使得线程似乎不争用相同的内存位置。

修订版评论,这就是整件事。实际上,这更进一步,因为它将大部分程序转换为0对象,并且还缩放了每个stateChanged = false;的线程数。它需要符合C ++ 11的编译器。

std::array<sf::Color, MAX+1> colors;
for (int i=0; i <= MAX; ++i) {
    colors[i] = getColor(i);
}


评论


\ $ \ begingroup \ $
您是否衡量了该多线程实现?在映像的每一列启动一个线程的开销可能过于精细。此外,对于通常的CPU,启动IMAGE_WIDTH线程的可能性很大,这将导致不必要的许多上下文切换。
\ $ \ endgroup \ $
–没人远离SE
16-3-31在19:14



\ $ \ begingroup \ $
是的,我测量了它,并且速度更快。我认为您误读了代码。不是图像的每列一个线程是1000个线程,而是每100条水平线一个线程,对于此代码而言是6个线程。
\ $ \ endgroup \ $
–爱德华
16年3月31日在19:17

\ $ \ begingroup \ $
是的,我错过了+ =100。但这是一个神奇的数字,不会随着图像大小/核心数的不同而自动缩放。
\ $ \ endgroup \ $
–没人远离SE
16年3月31日在19:19

\ $ \ begingroup \ $
您能给出您最终代码的完整清单吗?我想对其进行测量,但是很难重新创建它。
\ $ \ endgroup \ $
–没人远离SE
16年3月31日在19:41

\ $ \ begingroup \ $
@没人:好!我期待您的计时结果。我将完整的代码发布到我的答案中。
\ $ \ endgroup \ $
–爱德华
16年3月31日在21:35

#2 楼

命名


与Python不同,全大写名称通常留给C ++中的宏(顺便要避免)。例如:M_PICHAR_BIT等。

现代语言功能

在很多地方都可以利用现代C ++功能:




constexpr :纯常数和幻数应声明为constexpr

static constexpr int image_width  = 1000;
static constexpr int image_height = 600;

static constexpr int max_iterations  = 127;


常量也可以在命名空间(例如namespace constants)中或在enum下收集。


统一初始化:您可以通过不编写显式类型来使用它来简化某些部分。
getColor中,更喜欢: >
sf::Color getColor(int iterations) 
{
     int r, g, b;

     // ...

     return {r, g, b};
}



性能

sf::RenderWindow进行的次要优化将尝试通过赋予sf::Color默认值来减少条件赋值,因此仅在必要时进行更改:

sf::RenderWindow window { {IMAGE_WIDTH, IMAGE_HEIGHT}, "Mandelbrot"};


其他详细信息


通常应避免使用全局变量;
getColor r, g, b可以默认为maximum,即mandelbrot


MAX应该具有最大级别,因此您应该确保它没有损坏(int mandelbrot(double startReal, double startImag, int maximum = ::MAX) -s是zoom)。


评论


\ $ \ begingroup \ $
您能详细介绍一下constexpr吗?为什么它比const更受欢迎?
\ $ \ endgroup \ $
–杰瓦
16-3-31在18:42

\ $ \ begingroup \ $
@jacwah const告诉编译器,该值在程序的整个生命周期中都不会改变;但是,它没有说何时将分配此类常量。如果在运行时完成,那将是非常好的。 constexpr不允许这样做:值必须严格为常量表达式,编译器应确保该值。有关更多信息,请参见此处。
\ $ \ endgroup \ $
–edmz
16年3月31日在19:02

\ $ \ begingroup \ $
@jacwah这种“不足”也可能会引起误解。请参阅此处的示例。
\ $ \ endgroup \ $
–edmz
16年3月31日在19:06

\ $ \ begingroup \ $
@jacwah因此,如果您确定所拥有的数据,则最好使用const。在某些情况下(例如模板和一般元编程)也需要它。它还可能有助于产生更快的代码。
\ $ \ endgroup \ $
–edmz
16年3月31日在19:20

#3 楼

在这里,我将主要评论您代码的性能方面。应该由比我更精通C ++的人来改进代码的样式部分。 ,没有说明用途的数字,以及使用这些数字需要在更改位置时更新多个位置的值。也许您想使它们成为常量,分别将它们分别命名为0.940。乘法,速度更快,因此可以使用这些选项进行编译以发布。为了补偿浮点误差,您应该考虑一个小的Epsilon(最大允许偏差)值约10-15。尝试将救援条件更改为<= 4.0 + 10-15。应当进行Mandelbrot计算的图像像素范围。在每个因平移而无效的像素范围内的平移事件上调用此函数,获取具有空白像素的该图像并将其与旧图像合并以获得平移图像。应该使平移数量级更快。






ZOOM_DOWN_FACTOR 是不必要的。一个ZOOM_UP_FACTOR可以在这里正常工作。应避免潜在的内存泄漏。 (感谢@pacmaninbw将此通知我)。

(经验丰富的)分形程序员的一些有用的技巧:
(我当然是。请看这里,尤其是熟悉的人Java)

实施适当形式的颜色平滑。线性插值应该有所帮助。它的性能有点昂贵,但可以大大提高图像质量。指数平滑应该有助于提供合适的插值。

这是公式: -O3。在这里, -ffastmath是标准指数函数e ^ x,您的数学库中已有它。 pow()main()是复数,counter >= maximum是上一次迭代中==的值。 return返回break的模数的平方(基本上是window.close()的实部和虚部的平方和)。 case是小数或平滑迭代计数。
对于每个RGB分量,按如下所示应用线性插值:
expiter += exp(-cabs(z)-0.5/(cabs(zold - z)))exp(x)zold基本上是zzold的RGB分量的值

评论


\ $ \ begingroup \ $
-O3 +1。我记得当我开始学习C ++并首次尝试使用-O3时,我在光线跟踪器上的手动优化超出了一个数量级。
\ $ \ endgroup \ $
–没人远离SE
16-3-31在13:04

\ $ \ begingroup \ $
谢谢,这对您很有帮助!色彩平滑听起来不错,所以我会尽快研究。
\ $ \ endgroup \ $
–laurencevs
16-3-31在14:26

#4 楼

使用库功能

曼德博图像是用复数计算的。 C ++提供了<complex>标头,其中包含一个用于从浮点数或其他数字类型生成复数的模板。未初始化的变量,但通常最好在r函数开始时将gbgetColor值初始化为零。这也允许删除if s中所有以后的零分配。
由于代码使用的等距间距为16的倍数,因此您可以改用if:不同的像素彼此独立。这要求并行化。将OpenMP的并行用于其中可能就足够了。 br />

评论


\ $ \ begingroup \ $
感谢您的所有帮助。特别是,按照您建议的方式使用开关会使getColorfunction变得更加整洁!
\ $ \ endgroup \ $
–laurencevs
16年3月31日在14:32

\ $ \ begingroup \ $
@monopole:我认为这是一个hack,而不是惯用的方法,并且由于减少了分支,它旨在作为速度优化。
\ $ \ endgroup \ $
–没人远离SE
16-3-31在15:02



\ $ \ begingroup \ $
不再需要使用OpenMP了。 C ++带有自己的并行库。只需使用async()
\ $ \ endgroup \ $
–马丁·约克
16-3-31在16:01



\ $ \ begingroup \ $
您可能应该使用pow(x,2)(而不是2.0)来查看编译器是否对此进行了优化。
\ $ \ endgroup \ $
–马丁·约克
16 Mar 31 '16 at 16:02

\ $ \ begingroup \ $
@LokiAstari:尽管我同意标准C ++中存在允许并行化的可能性,但是在这种情况下,针对OpenMP并行的编写要容易得多。
\ $ \ endgroup \ $
–没人远离SE
16-3-31在16:06



#5 楼

首先,我要说这是一个非常酷而且制作精良的项目!如果您想提高代码的性能,建议将工作分担给GPU着色器。 mandelbrot很容易适合片段着色器的描述。您可以通过SFML使用OpenGL实现此目的,请参阅此内容。

#6 楼

命名,次要但有用

int mandelbrot-此函数返回迭代次数,名称mandelbrot并未告知其用途。名称int counter = 0;会更好,因为它可以解释更多信息

#7 楼

我看到您已经获得了许多有关良好代码实践的答案,例如命名约定和其他好东西。以下是一般现代CPU的计算算法的一些优化帮助。这对于避免冒险破坏现代处理器中的指令流水非常重要。

代码示例:mandelbrot和getColor函数中的if和whiles。编译器。如果可能的话,允许它进行优化,尝试和展开循环,通过让它知道您拥有的CPU来帮助它选择好的汇编指令。如果函数调用处于循环中,则允许它内联函数调用。如果可以降低精度,则可以使用快速指令和较低的精度(例如,用float代替double)。我怀疑您可以在此特定程序上使用快速指令而不会出现任何问题。

代码示例:允许编译器进行内联可以避免分支到mandelbrot和getColor函数的调用(它们位于循环)。


内存规划和结构化。规划数据结构和计算,以尽可能多地利用RAM缓存。丢失和命中的缓存之间的差异很容易影响性能数十至数百倍。