因为每次缩放或平移后都会生成新图像,所以它非常慢,特别是对于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键)后的窗口:
#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));
}
}
由于仅
real
和imag
在循环内发生了变化,因此用以下所有内容替换实际上很简单: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
循环。接下来,对
MAX
和for
值进行了预先计算,使代码更短,更简单(并且可能更有效)。 最后,早期的救助是在循环内完成的。只有在循环结束时,才返回值
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_PI
,CHAR_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.9
和40
。乘法,速度更快,因此可以使用这些选项进行编译以发布。为了补偿浮点误差,您应该考虑一个小的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
基本上是z
和zold
的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
函数开始时将g
,b
和getColor
值初始化为零。这也允许删除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缓存。丢失和命中的缓存之间的差异很容易影响性能数十至数百倍。
评论
您的审查目标是什么?速度?可读性好吗?@没人,我对Java和Python更加熟悉,所以我有兴趣使我的代码与C ++风格更加一致。可读性也是我关注的问题。对我而言,速度并不是优先考虑的事项,因为我认为我对改进的地方有一个大概的认识。
您在执行window.close()之后调用window.clear(),window.draw()和window.display(),您真的要这样做吗?
o_O花费了整整5分钟的时间才半秒回想。我们取得了多少进展。
还可以选择保存图像缓冲区然后替换整个图像的方法,而不是按像素操作和调用函数吗?