Skip to content

Commit

Permalink
NEW/REWORKED JKQTBasePlottercan be used is re-entrant, i.e. different…
Browse files Browse the repository at this point in the history
… instances can be used from different threads in parallel (although there is significant overhead due to shared caches between the threads!).

NEW added multithreaded example to demonstrate using JKQTBasePlotter in several parallel threads
  • Loading branch information
jkriege2 committed Jan 4, 2024
1 parent 3b598f4 commit aa2fcb1
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 4 deletions.
5 changes: 3 additions & 2 deletions doc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ if(JKQtPlotter_BUILD_EXAMPLES)
datastore_regression/datastore_regression,datastore_regression_lin,datastore_regression_linrobust,datastore_regression_linrobust_p,datastore_regression_linweight,datastore_regression_nonlinreg_exp,datastore_regression_nonlinreg_pow,datastore_regression_polynom,datastore_regression_polynom_errros/--iteratefunctorsteps
datastore_statistics/datastore_statistics,datastore_statistics_dataonly,datastore_statistics_boxplots_simple,datastore_statistics_boxplots_outliers,datastore_statistics_hist,datastore_statistics_kde,datastore_statistics_cumhistkde/--iteratefunctorsteps
datastore_statistics_2d/datastore_statistics_2d
multithreaded/multithreaded/--mdfile=${CMAKE_CURRENT_LIST_DIR}/../examples/multithreaded/README.md
)


Expand Down Expand Up @@ -279,7 +280,7 @@ if(JKQtPlotter_BUILD_EXAMPLES)
foreach(ex ${JKQTPlotter_GenerateDocScreenshots_From})
set(example ${ex})
set(basename ${ex})
string(REGEX MATCH "(.+)/(.+)" dummy ${ex})
string(REGEX MATCH "([^/]+)/([^/]+)" dummy ${ex})
set(extra_command "")
if(CMAKE_MATCH_1 STREQUAL "" OR CMAKE_MATCH_2 STREQUAL "")
set(example ${ex})
Expand All @@ -288,7 +289,7 @@ if(JKQtPlotter_BUILD_EXAMPLES)
set(example ${CMAKE_MATCH_1})
set(basename ${CMAKE_MATCH_2})
set(CMAKE_MATCH_3 "")
string(REGEX MATCH "(.+)/(.*)/(.+)" dummy ${ex})
string(REGEX MATCH "([^/]+)/([^/]*)/(.+)" dummy ${ex})
if(NOT (CMAKE_MATCH_3 STREQUAL ""))
set(example ${CMAKE_MATCH_1})
set(basename ${CMAKE_MATCH_2})
Expand Down
11 changes: 10 additions & 1 deletion doc/dox/examples_and_tutorials.dox
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ All test-projects are Qt-projects that use qmake to build. You can load them int
<td> `JKQTPXYLineGraph` and `JKQTPFilledVerticalRangeGraph` <br> C++ vector of data <br> date/time axes <br> plot min/max range graph <br> internal LaTeX parser <br> data from CSV files
<tr><td> \image html second_axis_small.png
<td> \subpage JKQTPlotterSecondaryAxes
<td> plottig with secondary axes, `JKQTPBasePlotter::addSecondaryXAxis()`/`JKQTPBasePlotter::addSecondaryYAxis()`
<td> plottig with secondary axes, `JKQTBasePlotter::addSecondaryXAxis()`/`JKQTBasePlotter::addSecondaryYAxis()`
<tr><td> \image html advancedlineandfillstyling_small.png
<td> \subpage JKQTPlotterAdvancedLineAndFillStyling
<td> `JKQTPXYLineGraph`, `JKQTPSpecialLineHorizontalGraph` and `JKQTPBarVerticalGraph` <br> C++ vector of data <br> advanced line styling and filling
Expand Down Expand Up @@ -252,6 +252,15 @@ All test-projects are Qt-projects that use qmake to build. You can load them int
<td> Allows to zoom into the Mandelbrot Set, using the different Zooming methods of JKQTPlotter
</table>

\subsection jkqtp_extut_specialusecasesexamples Examples for special Use-Cases

<table>
<tr><th> Screenshot <th> Description <th> Notes
<tr><td> \image html multithreaded_small.png
<td> \subpage JKQTPlotterMultiThreaded
<td> multi-threaded plotting using JKQTBasePlotter
</table>


\subsection jkqtp_extut_cmake_build Examples for CMake Build System

Expand Down
6 changes: 5 additions & 1 deletion doc/dox/jkqtplotter_usage.dox
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@
With simlar code you can also integrate JKQTBasePlotter into your own widgets.



This class is immpleented in a such a way that ^different instances can be used in different parallel threads, i.e. the class is re-entrant.
There are however access to different cached data is synchronized between all threads (i.e. static internal caches are used), which limmits
the acheavable parallelization speedup!

\see See \ref JKQTPlotterMultiThreaded for an example of multi-threaded plotting!



Expand Down
2 changes: 2 additions & 0 deletions doc/dox/whatsnew.dox
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Changes, compared to \ref page_whatsnew_V4_0_0 "v4.0.0" include:
<li>FIXED issue <a href="https://github.com/jkriege2/JKQtPlotter/pull/99">#99: Clipping of Tick Labels</a>: for horizontal axes, additional space at the left and/or right of the plot is allowed so labels are no longer clipped (thanks to <a href="https://github.com/allenbarnett5">user:allenbarnett5/a> for reporting)</li>
<li>FIXED issue <a href="https://github.com/jkriege2/JKQtPlotter/pull/99">#99: Height of one-column key/legend was too large</a> (thanks to <a href="https://github.com/allenbarnett5">user:allenbarnett5/a> for reporting)</li>
<li>FIXED issue mentioned in <a href="https://github.com/jkriege2/JKQtPlotter/pull/110">#110: Lock the panning action to certain values: View zooms in, when panning close to AbosluteXY</a> (thanks to <a href="https://github.com/sim186">user:sim186</a> for reporting)</li>
<li>FIXED: jkqtpstatSum() and jkqtpstatSumSqr() did not work, as a non-existing function is called internally</li>
<li>FIXED/IMPROVED issue <a href="https://github.com/jkriege2/JKQtPlotter/issues/100">#100: Add option to disable resize delay feature by setting the delay to zero</a> (thanks to <a href="https://github.com/fpalazzolo">user:fpalazzolo</a> for reporting)</li>
<li>FIXED/NEW: placement of plot-title (was not centerd in its box, but glued to the bottom) by adding a plotstyle parameter JKQTBasePlotterStyle::plotLabelOffset</li>
<li>FIXED/REWORKED issue <a href="https://github.com/jkriege2/JKQtPlotter/issues/111">#111: Can't write to PDF files with JKQTPlotter::saveImage() when passing a filename ending in ".pdf"</a> (thanks to <a href="https://github.com/fpalazzolo">user:fpalazzolo/a> for reporting):<br/>While fixing this issue, the functions JKQTBasePlotter::saveImage() etc. gained a bool return value to indicate whether sacing was successful.</li>
Expand Down Expand Up @@ -108,6 +109,7 @@ Changes, compared to \ref page_whatsnew_V4_0_0 "v4.0.0" include:
<li>NEW: added JKQTBasePlotterStyle::plotLabelTopBorder to set the spacing between top and plot label</li>
<li>NEW: Due to addition of JKQTMathText::setFontOptions() and the matchign extension of JKQTMathText::setFontSpecial() (see below) you can now add modifiers like <tt>+BOLD+ITALIC</tt> to any font-name provided to JKQTPlotter and in style INI-files</li>
<li>NEW: added JKQTPLabelMinBesides and JKQTPLabelMaxBesides to JKQTPLabelPosition, so labels can be set besides the axes</li>
<li>NEW/REWORKED JKQTBasePlottercan be used is re-entrant, i.e. different instances can be used from different threads in parallel (although there is significant overhead due to shared caches between the threads!). This is demonstrated and discussed in \ref JKQTPlotterMultiThreaded . </li>
</ul></li>

<li>JKQTMathText:<ul>
Expand Down
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ add_subdirectory(impulsesplot)
add_subdirectory(logaxes)
add_subdirectory(mandelbrot)
add_subdirectory(multiplot)
add_subdirectory(multithreaded)
add_subdirectory(parametriccurve)
add_subdirectory(paramscatterplot)
add_subdirectory(paramscatterplot_image)
Expand Down
33 changes: 33 additions & 0 deletions examples/multithreaded/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.16)

set(EXAMPLE_NAME multithreaded)
set(EXENAME jkqtptest_${EXAMPLE_NAME})

message( STATUS ".. Building Example ${EXAMPLE_NAME}" )


# Set up source files
set(SOURCES multithreaded.cpp )
set(HEADERS multithreaded_thread.h )
set(RESOURCES )
set(UIS )

add_executable(${EXENAME} WIN32 ${SOURCES} ${HEADERS} ${RESOURCES} ${UIS})
target_link_libraries(${EXENAME} JKQTPExampleToolsLib)
target_include_directories(${EXENAME} PRIVATE ../../lib)
if(JKQtPlotter_BUILD_STATIC_LIBS)
target_link_libraries(${EXENAME} JKQTPlotterLib)

elseif(JKQtPlotter_BUILD_SHARED_LIBS)
target_link_libraries(${EXENAME} JKQTPlotterSharedLib)
endif()

# precomiled headers to speed up compilation
target_precompile_headers(${EXENAME} PRIVATE ../../lib/jkqtplotter/private/jkqtplotter_precomp.h)


# Installation
install(TARGETS ${EXENAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

#Installation of Qt DLLs on Windows
jkqtplotter_deployqt(${EXENAME})
110 changes: 110 additions & 0 deletions examples/multithreaded/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Example (JKQTPlotter): Multi-Threaded (Parallel) Plotting {#JKQTPlotterMultiThreaded}
This project (see `./examples/multithreaded/`) shows how to use JKQTBasePlotter in multiple threads in parallel.

The source code of the main application can be found in [`multithreaded.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/multithreaded/multithreaded.cpp) and [`multithreaded_thread.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/multithreaded/multithreaded_thread.cpp).

The file [`multithreaded_thread.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/multithreaded/multithreaded_thread.cpp) contains a [`QThread`](https://doc.qt.io/qt-6/qthread.html) class that implements the actual plotting within a static method that is also run inside the thread's [`QThread::run()`](https://doc.qt.io/qt-6/qthread.html#run) method. It generates a plot with several line-graphs and then saves them into a PNG-file:

```.cpp
public:
inline static QString plotAndSave(const QString& filenamepart, int plotIndex, int NUM_GRAPHS, int NUM_DATAPOINTS, double* runtimeNanoseconds=nullptr) {
QElapsedTimer timer;
timer.start();
const QString filename=QDir(QDir::tempPath()).absoluteFilePath(QString("testimg_%1_%2.png").arg(filenamepart).arg(plotIndex));
JKQTBasePlotter plot(true);

const size_t colX=plot.getDatastore()->addLinearColumn(NUM_DATAPOINTS, 0, 10, "x");
QRandomGenerator rng;
for (int i=0; i<NUM_GRAPHS; i++) {
JKQTPXYLineGraph* g;
plot.addGraph(g=new JKQTPXYLineGraph(&plot));
g->setXColumn(colX);
g->setYColumn(plot.getDatastore()->addColumnCalculatedFromColumn(colX, [&](double x) { return cos(x+double(i)/8.0*JKQTPSTATISTICS_PI)+rng.generateDouble()*0.2-0.1;}));
g->setTitle(QString("Plot %1: $f(x)=\\cos\\leftx+\\frac{%1\\pi}{8}\\right)").arg(i+1));
g->setDrawLine(true);
g->setSymbolType(JKQTPNoSymbol);

}
plot.setPlotLabel(QString("Test Plot %1").arg(plotIndex+1));
plot.getXAxis()->setAxisLabel("x-axis");
plot.getYAxis()->setAxisLabel("y-axis");
plot.zoomToFit();
plot.saveAsPixelImage(filename, false, "PNG");

if (runtimeNanoseconds) *runtimeNanoseconds=timer.nsecsElapsed();
return filename;
}

// ...

protected:
inline virtual void run() {
m_filename=plotAndSave(m_filenamepart, m_plotindex, m_NUM_GRAPHS, m_NUM_DATAPOINTS, &m_runtimeNanoseconds);
}
```
The main application in [`multithreaded.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/multithreaded/multithreaded.cpp) then uses this method/thread-class to perform a test: First the function is run several times serially and then an equal amount of times in parallel.
```.cpp
#define NUM_PLOTS 8
#define NUM_GRAPHS 6
#define NUM_DATAPOINTS 1000
QElapsedTimer timer;
/////////////////////////////////////////////////////////////////////////////////
// serial plotting
/////////////////////////////////////////////////////////////////////////////////
timer.start();
for (int i=0; i<NUM_PLOTS; i++) {
PlottingThread::plotAndSave("serial", i, NUM_GRAPHS, NUM_DATAPOINTS);
}
const double durSerialNano=timer.nsecsElapsed();
qDebug()<<"durSerial = "<<durSerialNano/1e6<<"ms";
/////////////////////////////////////////////////////////////////////////////////
// parallel plotting
/////////////////////////////////////////////////////////////////////////////////
QList<QSharedPointer<PlottingThread>> threads;
for (int i=0; i<NUM_PLOTS; i++) {
qDebug()<<" creating thread "<<i;
threads.append(QSharedPointer<PlottingThread>::create("parallel",i, NUM_GRAPHS, NUM_DATAPOINTS, nullptr));
}
timer.start();
for (int i=0; i<NUM_PLOTS; i++) {
qDebug()<<" staring thread "<<i;
threads[i]->start();
}
for (int i=0; i<NUM_PLOTS; i++) {
qDebug()<<" waiting for thread "<<i;
threads[i]->wait();
}
const double durParallelNano=timer.nsecsElapsed();
qDebug()<<"durParallel = "<<durParallelNano/1e6<<"ms";
threads.clear();
```

This test results in the following numbers (on my AMD Ryzen5 8/16-core laptop):

[comment]:RESULTS

<u><b>SERIAL RESULTS:</b></u><br/>runtime, overall = 1719.3ms<br/>single runtimes = (214.8 +/- 277.4) ms<br/>speedup = 1.00x<br/>threads / available = 1 / 16<br/><br/>

<u><b>PARALLEL RESULTS:</b></u><br/>
runtime, overall = 649.1ms<br/>single runtimes = (605.2 +/- 81.8) ms<br/>speedup = 7.46x<br/>threads / available = 8 / 16<br/><br/><b>speedup vs. serial = 2.6x</b>

[comment]:RESULTS_END

From this data you can observe:
- The plotting parallelizes nicely, i.e. the speedup ist >7x on a 8-core-machine. This is the speedup calculated as sum of runtimes of each thread, divided by the runtime of all threads in parallel.
- BUT: the speedup of serialized plotting vs. parallel plotting is way smaller: It is only 2-3x. This can be explained by the (significant) overhead due to shared caches (and therefore synchronization) between the plotters. This may be reworked in future!
- The variance in runtimes in the (initial) serial test-run is larger than in the parallel run. This is due to filling of the internal caches during the first plotting!
.

Finally the application displays the plots:

![multithreaded](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/multithreaded.png)

152 changes: 152 additions & 0 deletions examples/multithreaded/multithreaded.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/** \example multithreaded.cpp
* Using JKQTBasePlotter in multiple threads in parallel.
*
* \ref JKQTPlotterMultiThreaded
*/

#include <QApplication>
#include <QMainWindow>
#include <QLabel>
#include <QBoxLayout>
#include <QVector>
#include <QElapsedTimer>
#include <QFile>
#include <thread>
#include "multithreaded_thread.h"
#include "jkqtmath/jkqtpstatbasics.h"
#include "jkqtpexampleapplication.h"

#define NUM_SHOWN_PLOTS 3
#define NUM_PLOTS 8
#define NUM_GRAPHS 6
#define NUM_DATAPOINTS 1000


int main(int argc, char* argv[])
{
JKQTPAppSettingController highDPIController(argc,argv);
JKQTPExampleApplication app(argc, argv);
QMainWindow* mainWin=new QMainWindow();
mainWin->setWindowTitle("Multi-Threaded Plotting");
QWidget* main;
mainWin->setCentralWidget(main=new QWidget(mainWin));

QString markdownFile="";
for (int i=1; i<argc; i++) {
if (QString(argv[i]).startsWith("--mdfile=")) {
markdownFile=QString::fromLatin1(argv[i]).right(QString::fromLatin1(argv[i]).size()-9);
if (markdownFile.startsWith('"')) markdownFile.remove('"');
}
}


int result=0;
QStringList filenamesSerial, filenamesParallel;
{

QHBoxLayout* mainLayout=new QHBoxLayout(main);
main->setLayout(mainLayout);
QVBoxLayout* lay_serial=new QVBoxLayout();
QVBoxLayout* lay_parallel=new QVBoxLayout();
mainLayout->addLayout(lay_serial);
QLabel* l;
lay_serial->addWidget(l=new QLabel("Serialized Plotting"));
QFont f=l->font();
f.setBold(true);
f.setPointSize(16);
l->setFont(f);
lay_parallel->addWidget(l=new QLabel("Parallel Plotting"));
l->setFont(f);

QLabel* labSerialResult=new QLabel(main);
lay_serial->addWidget(labSerialResult);
QLabel* labParallelResult=new QLabel(main);
lay_parallel->addWidget(labParallelResult);

mainLayout->addLayout(lay_parallel);
QVector<QLabel*> pic_parallel, pic_serial;
for (int i=0; i<NUM_SHOWN_PLOTS; i++) {
pic_serial.push_back(new QLabel(main));
pic_parallel.push_back(new QLabel(main));
lay_serial->addWidget(pic_serial.last(), 1);
lay_parallel->addWidget(pic_parallel.last(), 1);
}

QElapsedTimer timer;
QList<double> runtimesSerial;
timer.start();
for (int i=0; i<NUM_PLOTS; i++) {
double dur=0;
filenamesSerial<<PlottingThread::plotAndSave("serial", i, NUM_GRAPHS, NUM_DATAPOINTS, &dur);
runtimesSerial<<dur/1e6;
}
const double durSerialNano=timer.nsecsElapsed();
qDebug()<<"durSerial = "<<durSerialNano/1e6<<"ms";

QList<double> runtimesParallel;
QList<QSharedPointer<PlottingThread>> threads;
for (int i=0; i<NUM_PLOTS; i++) {
qDebug()<<" creating thread "<<i;
threads.append(QSharedPointer<PlottingThread>::create("parallel",i, NUM_GRAPHS, NUM_DATAPOINTS, nullptr));
}
timer.start();
for (int i=0; i<NUM_PLOTS; i++) {
qDebug()<<" staring thread "<<i;
threads[i]->start();
}
for (int i=0; i<NUM_PLOTS; i++) {
qDebug()<<" waiting for thread "<<i;
if (threads[i]->wait()) {
filenamesParallel<<threads[i]->getFilename();
runtimesParallel<<threads[i]->getRuntimeNanosends()/1e6;
}
}
const double durParallelNano=timer.nsecsElapsed();
qDebug()<<"durParallel = "<<durParallelNano/1e6<<"ms";

threads.clear();

for (int ii=0; ii<NUM_SHOWN_PLOTS; ii++) {
int i=ii;
if (ii>NUM_SHOWN_PLOTS/2) i=NUM_PLOTS-1-NUM_SHOWN_PLOTS+ii;
pic_serial[ii]->setPixmap(QPixmap(filenamesSerial[i], "PNG"));
pic_parallel[ii]->setPixmap(QPixmap(filenamesParallel[i], "PNG"));
}
QString ser_result, par_result;
labSerialResult->setText(ser_result=QString("runtime, overall = %1ms<br/>single runtimes = (%2 +/- %3) ms<br/>speedup = %4x<br/>threads / available = %5 / %6<br/><br/> ").arg(durSerialNano/1e6,0,'f',1).arg(jkqtpstatAverage(runtimesSerial.begin(), runtimesSerial.end()),0,'f',1).arg(jkqtpstatStdDev(runtimesSerial.begin(), runtimesSerial.end()),0,'f',1).arg(jkqtpstatSum(runtimesSerial.begin(), runtimesSerial.end())/(durSerialNano/1e6),0,'f',2).arg(1).arg(std::thread::hardware_concurrency()));
labParallelResult->setText(par_result=QString("runtime, overall = %1ms<br/>single runtimes = (%2 +/- %3) ms<br/>speedup = %4x<br/>threads / available = %5 / %6<br/><br/><b>speedup vs. serial = %7x</b>").arg(durParallelNano/1e6,0,'f',1).arg(jkqtpstatAverage(runtimesParallel.begin(), runtimesParallel.end()),0,'f',1).arg(jkqtpstatStdDev(runtimesParallel.begin(), runtimesParallel.end()),0,'f',1).arg(jkqtpstatSum(runtimesParallel.begin(), runtimesParallel.end())/(durParallelNano/1e6),0,'f',2).arg(NUM_PLOTS).arg(std::thread::hardware_concurrency()).arg(durSerialNano/durParallelNano,0,'f',1));
mainWin->show();

if (!markdownFile.isEmpty()) {
qDebug()<<"modifying MD-file "<<markdownFile;
QFile f(markdownFile);
QByteArray md;
if (f.open(QFile::ReadOnly|QFile::Text)) {
md=f.readAll();
qDebug()<<" read "<<md.size()<<" bytes";
f.close();
const auto istart=md.indexOf("[comment]:RESULTS");
const auto iend=md.indexOf("[comment]:RESULTS_END");
qDebug()<<" istart="<<istart<<", iend="<<iend;
if (istart>=0 && iend>istart) {
const QByteArray newResults="[comment]:RESULTS\n\n<u><b>SERIAL RESULTS:</b></u><br/>"+ser_result.toUtf8()
+"\n\n<u><b>PARALLEL RESULTS:</b></u><br/>\n"+par_result.toUtf8()+"\n\n";
md.replace(istart,iend-istart,newResults);
if (f.open(QFile::WriteOnly)) {
qDebug()<<" writing "<<md.size() <<"bytes";
f.write(md);
f.close();
}
}
}
} else {
qDebug()<<"no MD-file given";
}

result = app.exec();
}
for (const auto& fn: (filenamesSerial+filenamesParallel)) {
QFile::remove(fn);
}
return result;
}
Loading

0 comments on commit aa2fcb1

Please sign in to comment.