I'm trying to implement the Mandelbrot set with VS 2022 Community Edition. I do calculations and SetPixel() calls in a 2-D loop. It works, but takes 7 seconds to render the image. I want to speed things up. One approach is to break the image into blocks and dispatch the work to multiple threads. Before I go to that trouble, I need to know if SetPixel() is thread safe. I did some research and the answer is unclear. It seems to me that if two threads are working on different areas of the image, that concurrent calls to SetPixel() should be OK. Is it? What about using a mutex to control access to SetPixel()? The second approach seems to be LockBits... I see a lot of posts that say that SetPixel() is slow, and that LockBits should be used. Problem is that I can't find any support for LockBits in my system, and none of the posts completely answer the question of how to do that. Thank you. My code follows...
case WM_PAINT:
{
if (bHoldPaint) break;
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
RECT rect;
GetClientRect(hWnd, &rect);
// Algorithm obtained from https://en.wikipedia.org/wiki/Mandelbrot_set.
double x0, y0, x, y, xtemp;
const int max_iterations = 1000;
int iteration;
int xPixel, yPixel;
for (yPixel = rect.top; yPixel < rect.bottom; ++yPixel)
{
for (xPixel = rect.left; xPixel < rect.right; ++xPixel)
{
x0 = (xMax - xMin) / (rect.right - rect.left) * xPixel + xMin;
y0 = (yMax - yMin) / (rect.bottom - rect.top) * yPixel + yMin;
x = 0.0;
y = 0.0;
iteration = 0;
while (x * x + y * y <= 2 * 2 && iteration < max_iterations)
{
xtemp = x * x - y * y + x0;
y = 2 * x * y + y0;
x = xtemp;
++iteration;
}
SetPixel(hdc, xPixel, yPixel, (COLORREF)(-16777215.0 / max_iterations * iteration + 16777215.0));
}
}
EndPaint(hWnd, &ps);
}
break;
A device context can safely be used from multiple threads concurrently. Raymond Chen covered the details in the blog post Thread affinity of user interface objects, part 2: Device contexts:
The article moves on to explain that users are still required to coordinate access. This involves making sure that threads don't step on other threads' feet by overwriting the same areas. Coordination could be implemented via synchronization primitives (such as mutexes), but that is not strictly required. A far cheaper (and easier) way to accomplish the same thing is by structuring the code so that no two threads will ever write to the same pixel.
If you want to experiment with multiple threads updating the same DC you're all set. Doing that is well-specified. In this first iteration of performance optimization you should replace
SetPixel()withSetPixelV(). The latter is faster as it doesn't read the previous color value (which you don't need anyway), but either one is incredibly expensive still.Once you have made progress with that there are more options to evaluate:
To do this, allocate a block of memory of sufficient size, write to it, and construct a bitmap over that data when done (using
CreateBitmap(), for example).LockBits()does pretty much that, but it is available in .NET'sSystem.Drawing.Bitmaptype only.WM_PAINTmessage handler:Message handlers are called in order. While a program is busy handling a message, no other messages are getting dispatched (this is roughly correct enough). You can move the calculation off the UI thread and when it retires, signal to the UI that an update is necessary. A call to
InvalidateRect()is sufficient.Spawning a thread is a relatively expensive operation in Windows. If you can reuse existing threads that have gone idle, it is a worthwhile optimization (or de-pessimization).
The specific workload here is incredibly well-suited for GPU calculation. The calculation of each pixel is independent of the results for any other pixel, making the problem embarrassingly parallelizable. GPUs commonly have hundreds (or thousands) of processing units that execute code in parallel so going GPU will make a significant difference. (It's also fairly involved getting started.)
There are other things you can try out, but this will get you started. Almost, anyway. The first step forward is familiarizing yourself with profiling.