I am new to parallel programming. I have been playing around with multi-threading and for some reason, multi-threading the Mandelbrot set is slower than running a single thread. I have been trying to figure it out for hours. Are there any improvements that you could suggest or perhaps something is wrong with some parts?

Mandelbrot.cpp:



// Write the image to a TGA file with the given name.
void Mandelbrot::write_tga(const char* filename)
{

ofstream outfile(filename, ofstream::binary);

0, // no image ID
0, // no colour map
2, // uncompressed 24-bit image
0, 0, 0, 0, 0, // empty colour map specification
0, 0, // X origin
0, 0, // Y origin
WIDTH & 0xFF, (WIDTH >> 8) & 0xFF, // width
HEIGHT & 0xFF, (HEIGHT >> 8) & 0xFF, // height
24, // bits per pixel
0, // image descriptor
};

for (int y = 0; y < HEIGHT; ++y)
{
for (int x = 0; x < WIDTH; ++x)
{
uint8_t pixel[3] = {
image[y][x] & 0xFF, // blue channel
(image[y][x] >> 8) & 0xFF, // green channel
(image[y][x] >> 16) & 0xFF, // red channel
};
outfile.write((const char*)pixel, 3);
}
}

outfile.close();
if (!outfile)
{
// An error has occurred at some point since we opened the file.
cout << "Error writing to " << filename << endl;
exit(1);
}
}

void Mandelbrot::printProgress() {
// *** CONSUMER *** //

while (progressPercentage <= 100.00)
{
//std::cout << "\nAquiring lock in PrintProgress\n";
unique_lock<mutex> lock(progressMutex);

{
progressConditionVariable.wait(lock);
}
//std::cout << "\nlock aquired\n";

cout << progressPercentage << endl;

}

}

void Mandelbrot::calcProgress() {
// *** PRODUCER *** //

//shared progress variable
//protect with a mutex
//print_func() waits to be signaled by worker threads and outputs
//condition variables

unique_lock<mutex> lock(progressMutex);
progressCount += 1;

lock.unlock();

// progress condition variable
progressConditionVariable.notify_one();

}

// Render the Mandelbrot set into the image array.
// The parameters specify the region on the complex plane to plot.
void Mandelbrot::compute_mandelbrot(double left, double right, double top, double bottom, int startNum, int endNum, int y)
{

// The number of times to iterate before we assume that a point isn't in the
// Mandelbrot set.
// (You may need to turn this up if you zoom further into the set.)
const int MAX_ITERATIONS = 500;

for (int x = 0; x < WIDTH ; ++x)
{

// Work out the point in the complex plane that
// corresponds to this pixel in the output image.
complex<double> c(left + (x * (right - left) / WIDTH),
top + (y * (bottom - top) / HEIGHT));

// Start off z at (0, 0).
complex<double> z(0.0, 0.0);

// Iterate z = z^2 + c until z moves more than 2 units
// away from (0, 0), or we've iterated too many times.
int iterations = 0;
while (abs(z) < 2.0 && iterations < MAX_ITERATIONS)
{
z = (z * z) + c;
++iterations;
}

if (iterations == MAX_ITERATIONS)
{
// z didn't escape from the circle.
// This point is in the Mandelbrot set.

image[y][x] = 0xFCC2E6; // darker pink
}
else
{
// z escaped within less than MAX_ITERATIONS
// iterations. This point isn't in the set.
//image[y][x] = 0xFFEAF7; // pink

int i = iterations;
if (i < 5)
image[y][x] = 0xFFEAF7;
else if (i < 10)
image[y][x] = 0xF9BCDD;
else if (i < 40)
image[y][x] = 0xD5A4CF;

}

image[endNum - 1][x] = 0xFF0000;

}
// call progress function
calcProgress();
}


Mandelbrot.h:

#pragma once

#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <complex>
#include <fstream>
#include <iostream>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <atomic>

#include "Mandelbrot.h"

const int WIDTH = 1020;  //ori 1920
const int HEIGHT = 600; // ori 1200

// Import things we need from the standard library
using std::chrono::duration_cast;
using std::chrono::milliseconds;
using std::complex;
using std::cout;
using std::endl;
using std::ofstream;
using std::mutex;
using std::condition_variable;
using std::unique_lock;

class Mandelbrot
{
private:

public:
// initialise variables
mutex progressMutex;
condition_variable progressConditionVariable;
int progressCount = 0;
double progressPercentage = 0;

// The image data.
// The size of the image to generate.
uint32_t image[HEIGHT][WIDTH];

//functions
void write_tga(const char* filename);
void printProgress();
void calcProgress();
void compute_mandelbrot(double left, double right, double top, double bottom, int startNum, int endNum, int y);

};



source.cpp:

#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <complex>
#include <fstream>
#include <iostream>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <atomic>

// Import things we need from the standard library
using std::chrono::duration_cast;
using std::chrono::milliseconds;
using std::complex;
using std::cout;
using std::endl;
using std::ofstream;
using std::mutex;
using std::condition_variable;
using std::unique_lock;

// Define the alias "the_clock" for the clock type we're going to use.

int main(int argc, char* argv[])
{
Mandelbrot* mandelbrot = new Mandelbrot;

// This shows the whole set.
int lastNum = 0;
int calcHeight = HEIGHT / mandelbrot->numOfThreads;
int End = calcHeight;

//lambda function

while (mandelbrot->progressPercentage <= 100.00) {
mandelbrot->printProgress();
}

});

// Start timing
the_clock::time_point start = the_clock::now();

for (int i = 0; i < mandelbrot->numOfThreads; i++)
{
//lambda function

for (int y = lastNum; y < End; ++y)
{
//std::cout << "Before compute\n";
mandelbrot->compute_mandelbrot(-2.0, 1.0, 1.125, -1.125, lastNum, End, y);
}
lastNum = End;
End = End + calcHeight;
});

}

// Wait for threads to finish.
for (auto &t : threads) {
t.join();
}

// Stop timing
the_clock::time_point end = the_clock::now();

Consumer.join();

// Compute the difference between the two times in milliseconds
auto time_taken = duration_cast<milliseconds>(end - start).count();
cout << "Computing the Mandelbrot set took " << time_taken << " ms." << endl;

mandelbrot->write_tga("output.tga");

return 0;
}

• Should keep in mind, if using synchronization mecanism like mutex this can make your code sometimes even slower. May 12 at 9:52

D. Jurcau already mentioned that you are not distributing your work correctly. It's even worse than his terse answer hints at. One issue is that inside the lambda function, which is run in its own thread, you are modifying lastNum and End, but those variables are shared among all threads. Because you are not using any kind of locking, there is just no telling what values these variables will hold, as multiple threads can update them at the same time.

You should figure out the range of y values a thread should work on outside that thread, and pass that range to the lambda by value, like so:

for (int i = 0; i < mandelbrot->numOfThreads; ++i)
{
for (int y = lastNum; y < End; ++y)
{
mandelbrow->compute_mandelbrot(..., y);
}
});

lastNum = End;
End += calcHeight;
}


Your way of distributing work over threads is very crude. Consider that some threads will finish more quickly than others; for example the regions that contain little to no black will be much faster to calculate than those which have a lot of black. Furthermore, why would 10 threads be the optimal number? This depends heavily on the number of CPU cores available. Ideally, you would split up the work into many small tasks (say, one line per task), and create a pool of worker threads corresponding to the number of CPU cores, and let each of those workers pick tasks at their own pace until all tasks have been done.

There are many implementations of thread pools here on Code Review that you can draw inspiration from.

You can use std::thread::hardware_concurrency() to find the number of threads that the hardware can run concurrently. For CPU-bound tasks, that's the appropriate number of threads to spawn.

Be sure to handle edge cases correctly

What if HEIGHT is not evenly divisible by mandelbrot->numOfThreads? That would cause the last few lines of the image to not be calculated. Make sure you handle edge cases correctly.

Your progress counter is using its own thread, and needs a mutex and a condition variable to coordinate with other threads when to print an update. For your program, this is overkill. Instead, you could just use a single mutex, and have compute_mandelbrot() itself print the progress right before it returns:

{
lock_guard lock(progressMutex);
progressCount++;
double progressPercentage = 100.0 * progressCount / numOfThreads;
cout << progressPercentage << '\n';
}


Alternatively, have the main thread call mandelbrot->printProgress() directly after starting all the worker threads.

Refactoring the code

The class Mandelbrot has a lot of functions, but while it clearly is designed to calculate the image using multiple threads, there is no member function that will manage those threads, that's left up to main(). This is a weird distribution of responsibilities. This might be a good time to rethink the structure of your code and to refactor it. Try to give classes only one responsibility each if possible. For example, have an Image class that just deals with images, nothing else. You might want to make a ThreadPool class to manage the threads, and even create a Timer class to do the time measurements.

Also note that not everything outside of main() needs to be in a class. Consider that if compute_mandelbrot() didn't have the responsibility of updating the progress counter, the only thing it needs apart from the parameters it already got is a reference to the image to store the results in. So it could just be a stand-alone function.

A good way to start refactoring the code is to try to imagine main() as concise as possible, for example you might want it to look like this:

int main()
{
Image image(WIDTH, HEIGHT);
Timer timer;

{

for (int y = 0; y < HEIGHT; y++) {