-
Notifications
You must be signed in to change notification settings - Fork 193
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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!). NEW added multithreaded example to demonstrate using JKQTBasePlotter in several parallel threads
- Loading branch information
Showing
11 changed files
with
389 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.