Skip to content

Commit

Permalink
MODIFIED JKQTPDataCache to implement a limited-size cache with least-…
Browse files Browse the repository at this point in the history
…recently-used delete stategy
  • Loading branch information
jkriege2 committed Jan 24, 2024
1 parent 868d6dc commit 1bf6c43
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 32 deletions.
4 changes: 2 additions & 2 deletions examples/multithreaded/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ This test results in the following numbers (on my AMD Ryzen5 8/16-core laptop):
<b>VERSION:</b> 5.0.0
<b>BUILD MODE:</b> Release

<u><b>SERIAL RESULTS:</b></u><br/>runtime, overall = 2336.7ms<br/>single runtimes = (97.3 +/- 158.4) ms<br/>speedup = 1.00x<br/>threads / available = 1 / 16<br/><br/><br/>
<u><b>SERIAL RESULTS:</b></u><br/>runtime, overall = 4241.3ms<br/>single runtimes = (176.7 +/- 319.6) ms<br/>speedup = 1.00x<br/>threads / available = 1 / 16<br/><br/><br/>

<u><b>PARALLEL RESULTS:</b></u><br/>
runtime, overall = 527.2ms<br/>single runtimes = (166.4 +/- 8.1) ms<br/>speedup = 7.58x<br/>threads / available = 8 / 16<br/>batch runs = 3<br/><br/><b>speedup vs. serial = 4.4x</b>
runtime, overall = 1025.3ms<br/>single runtimes = (325.0 +/- 15.0) ms<br/>speedup = 7.61x<br/>threads / available = 8 / 16<br/>batch runs = 3<br/><br/><b>speedup vs. serial = 4.1x</b>

[comment]:RESULTS_END

Expand Down
127 changes: 111 additions & 16 deletions lib/jkqtcommon/jkqtpcachingtools.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,24 @@
#define JKQTPCACHINGTOOLS_H

#include "jkqtcommon/jkqtcommon_imexport.h"
#include "jkqtcommon/jkqtpmathtools.h"
#include <QReadWriteLock>
#include <QReadLocker>
#include <QWriteLocker>
#include <functional>
#include <chrono>
#include <atomic>
#include <algorithm>
#include <unordered_map>

/** \brief tag type to configure JKQTPDataCache for thread-safety
* \ingroup jkqtptools_concurrency
*/
struct JKQTPDataCacheThreadSafe {};
/** \brief tag type to configure JKQTPDataCache for non thread-safety
* \ingroup jkqtptools_concurrency
*/
struct JKQTPDataCacheNotThreadSafe {};

/** \brief this class can be used to implement a general cache for values
* \ingroup jkqtptools_concurrency
Expand All @@ -45,51 +59,132 @@
*
* Internally the cache maps TKey to TData, but the signature of the get()-function and the generator functor actually uses TKeyInSignature,
* which may differ from TKey. The only limitation is that TKeyInSignature can be converted/assigned to a TKey
*
* The cache has a maximmum size m_maxEntries.
* When you try to add a new object, after which the size would grow beyond this, a fraction 1-m_retainFraction of elements are
* deleted from the cache. The delete strategy is least-recently used (LRU). In order to immplement this, the cache keeps track of
* the last use timestamp of each entry.
*
* You can deactivate the cleaning by setting m_maxEntries<0, but the the cache may grow indefinitely and there is possibly undefined behaviour
* when add one too many items!
*/
template <class TData, class TKey, bool ThreadSafe=true, class TKeyInSignature=TKey>
template <class TData, class TKey, typename ThreadSafe=JKQTPDataCacheThreadSafe, class TKeyInSignature=TKey>
struct JKQTPDataCache {
template <typename FF>
inline JKQTPDataCache(FF generateData):
m_generateData(std::forward<FF>(generateData))
inline JKQTPDataCache(FF generateData, int maxEntries=10000, double retainFraction=0.8):
m_maxEntries(maxEntries), m_retainFraction(retainFraction), m_generateData(std::forward<FF>(generateData))
{

}
JKQTPDataCache()=delete;
JKQTPDataCache(const JKQTPDataCache&)=delete;
JKQTPDataCache& operator=(const JKQTPDataCache&)=delete;
JKQTPDataCache(JKQTPDataCache&&)=default;
JKQTPDataCache& operator=(JKQTPDataCache&&)=default;

template<class... Args>
inline TData get_inline(Args... args) {
return get(TKeyInSignature(args...));
}

template <bool TSS=ThreadSafe>
inline TData get(const typename std::enable_if<TSS, TKeyInSignature>::type& key) {
template <typename TSS=ThreadSafe>
inline TData get(const typename std::enable_if<std::is_same<JKQTPDataCacheThreadSafe, TSS>::value, TKeyInSignature>::type& key) {
const TKey cacheKey=key;

QReadLocker lockR(&m_mutex);
if (m_cache.contains(cacheKey)) return m_cache[cacheKey];
auto it=m_cache.find(cacheKey);
if (m_cache.end()!=it) {
m_cacheLastUseTimestamps[cacheKey]->exchange(currenTimestamp());
return it->second;
}
lockR.unlock();

QWriteLocker lockW(&m_mutex);
if (m_cache.contains(cacheKey)) return m_cache[cacheKey];

it=m_cache.find(cacheKey);
if (m_cache.end()!=it) {
m_cacheLastUseTimestamps.at(cacheKey)->exchange(currenTimestamp());
return it->second;
}
if (m_maxEntries>0 && m_cache.size()>=static_cast<size_t>(m_maxEntries)) cleanCache_notThreadSafe();
m_cacheLastUseTimestamps.emplace(cacheKey, std::make_shared<std::atomic<int64_t>>(currenTimestamp()));
const auto newData=m_generateData(key);
m_cache[cacheKey]=newData;
m_cache.emplace(cacheKey,newData);
return newData;
}

template <bool TSS=ThreadSafe>
inline TData get(const typename std::enable_if<!TSS, TKeyInSignature>::type& key) {
template <typename TSS=ThreadSafe>
inline TData get(const typename std::enable_if<std::is_same<JKQTPDataCacheNotThreadSafe, TSS>::value, TKeyInSignature>::type& key) {
const TKey cacheKey=key;

if (m_cache.contains(cacheKey)) return m_cache[cacheKey];
auto it=m_cache.find(cacheKey);
if (m_cache.end()!=it) {
m_cacheLastUseTimestamps[cacheKey]->exchange(currenTimestamp());
return it->second;
}
if (m_maxEntries>0 && m_cache.size()>=static_cast<size_t>(m_maxEntries)) cleanCache_notThreadSafe();
const auto newData=m_generateData(key);
m_cache[cacheKey]=newData;
m_cache.emplace(cacheKey,newData);
m_cacheLastUseTimestamps.emplace(cacheKey, std::make_shared<std::atomic<int64_t>>(currenTimestamp()));
return newData;
}

template <typename TSS=ThreadSafe>
inline bool contains(const typename std::enable_if<std::is_same<JKQTPDataCacheThreadSafe, TSS>::value, TKeyInSignature>::type& key) const {
const TKey cacheKey=key;

QReadLocker lockR(&m_mutex);
return m_cache.find(cacheKey)!=m_cache.end();
}

template <typename TSS=ThreadSafe>
inline bool contains(const typename std::enable_if<std::is_same<JKQTPDataCacheNotThreadSafe, TSS>::value, TKeyInSignature>::type& key) const {
const TKey cacheKey=key;
return m_cache.find(cacheKey)!=m_cache.end();
}


inline int size() const {
return size_impl<ThreadSafe>();
}

private:
QHash<TKey, TData> m_cache;
mutable typename std::enable_if<ThreadSafe, QReadWriteLock>::type m_mutex;
template <typename TSS>
inline typename std::enable_if<std::is_same<JKQTPDataCacheThreadSafe, TSS>::value, int>::type size_impl() const {
QReadLocker lockR(&m_mutex);
return m_cache.size();
}

template <typename TSS>
inline typename std::enable_if<std::is_same<JKQTPDataCacheNotThreadSafe, TSS>::value, int>::type size_impl() const {
return m_cache.size();
}

/** \brief generate a timestamp */
static inline int64_t currenTimestamp() {
static auto firstTime=std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now()-firstTime).count();
}
/** \brief clean the cache, so at m_retainFraction*m_maxEntries entries remain. */
inline void cleanCache_notThreadSafe() {
if (m_maxEntries<0 || m_cache.size()<static_cast<size_t>(m_maxEntries)) return;
const int deleteItems=jkqtp_boundedRoundTo<int>(1, (1.0-m_retainFraction)*static_cast<double>(m_cache.size()), m_cache.size());
QList<QPair<TKey,int64_t> > allItems;
allItems.reserve(m_cacheLastUseTimestamps.size());
for (auto it=m_cacheLastUseTimestamps.begin(); it!=m_cacheLastUseTimestamps.end(); ++it) {
allItems.emplaceBack(it->first, it->second->load());
}
std::sort(allItems.begin(), allItems.end(), [](const QPair<TKey,int64_t>&a, const QPair<TKey,int64_t>&b) {return a.second>b.second;});
for (int i=0; i<deleteItems; i++) {
m_cache.erase(allItems[i].first);
m_cacheLastUseTimestamps.erase(allItems[i].first);
}
}
const int m_maxEntries;
const double m_retainFraction;
std::unordered_map<TKey, TData> m_cache;
std::unordered_map<TKey, std::shared_ptr<std::atomic<int64_t>>> m_cacheLastUseTimestamps;
mutable QReadWriteLock m_mutex;
const std::function<TData(TKeyInSignature)> m_generateData;
Q_DISABLE_COPY(JKQTPDataCache)
};

#endif // JKQTPCACHINGTOOLS_H
73 changes: 59 additions & 14 deletions lib/jkqtmathtext/jkqtmathtexttools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <QFont>
#include <QReadWriteLock>
#include <mutex>
#include <functional>


void initJKQTMathTextResources()
Expand Down Expand Up @@ -884,6 +885,9 @@ QPainterPath JKQTMathTextMakeHBracePath(double x, double ybrace, double width, d
namespace {

struct JKQTMathTextCacheKeyBase {
JKQTMathTextCacheKeyBase():
f(), ldpiX(0), ldpiY(0), pdpiX(0), pdpiY(0)
{}
inline explicit JKQTMathTextCacheKeyBase(const QFont& f_, QPaintDevice *pd):
f(f_)
{
Expand All @@ -904,7 +908,12 @@ namespace {

inline bool operator==(const JKQTMathTextCacheKeyBase& other) const{
return ldpiX==other.ldpiX && ldpiY==other.ldpiY && pdpiX==other.pdpiX && pdpiY==other.pdpiY && f==other.f;

};
inline bool operator<(const JKQTMathTextCacheKeyBase& other) const{
return ldpiX<other.ldpiX && ldpiY<other.ldpiY && pdpiX<other.pdpiX && pdpiY<other.pdpiY && f<other.f;
};
inline bool operator!=(const JKQTMathTextCacheKeyBase& other) const{
return !operator==(other);
};
};

Expand All @@ -931,6 +940,9 @@ namespace {

template <class TText=QString>
struct JKQTMathTextTBRDataH: public JKQTMathTextCacheKeyBase {
JKQTMathTextTBRDataH():
JKQTMathTextCacheKeyBase(), text("")
{}
inline explicit JKQTMathTextTBRDataH(const QFont& f_, const TText& text_, QPaintDevice *pd):
JKQTMathTextCacheKeyBase(f_, pd), text(text_)
{
Expand All @@ -940,7 +952,12 @@ namespace {

inline bool operator==(const JKQTMathTextTBRDataH& other) const{
return JKQTMathTextCacheKeyBase::operator==(other) && text==other.text;

};
inline bool operator!=(const JKQTMathTextTBRDataH& other) const{
return JKQTMathTextCacheKeyBase::operator!=(other) && text!=other.text;
};
inline bool operator<(const JKQTMathTextTBRDataH& other) const{
return JKQTMathTextCacheKeyBase::operator<(other) && text<other.text;
};
};

Expand All @@ -967,13 +984,41 @@ namespace {

}

template<>
struct std::hash<JKQTMathTextCacheKeyBase>
{
std::size_t operator()(const JKQTMathTextCacheKeyBase& data) const noexcept
{
return qHash(data.f)+std::hash<int>()(data.ldpiX)+std::hash<int>()(data.ldpiY)+std::hash<int>()(data.pdpiX)+std::hash<int>()(data.pdpiY);
}
};



template<>
struct std::hash<JKQTMathTextTBRDataH<QString>>
{
std::size_t operator()(const JKQTMathTextTBRDataH<QString>& data) const noexcept
{
return qHash(data.f)+qHash(data.text)+std::hash<int>()(data.ldpiX)+std::hash<int>()(data.ldpiY)+std::hash<int>()(data.pdpiX)+std::hash<int>()(data.pdpiY);
}
};

template<>
struct std::hash<JKQTMathTextTBRDataH<QChar>>
{
std::size_t operator()(const JKQTMathTextTBRDataH<QChar>& data) const noexcept
{
return qHash(data.f)+qHash(data.text)+std::hash<int>()(data.ldpiX)+std::hash<int>()(data.ldpiY)+std::hash<int>()(data.pdpiX)+std::hash<int>()(data.pdpiY);
}
};



QRectF JKQTMathTextGetTightBoundingRect(const QFont &f, const QString &text, QPaintDevice *pd)
{
//thread_local JKQTPDataCache<QRectF,JKQTMathTextTBRDataH,false,JKQTMathTextTBRDataHExt> cache(
static JKQTPDataCache<QRectF,JKQTMathTextTBRDataH<QString>,true,JKQTMathTextTBRDataHExt<QString>> cache(
static JKQTPDataCache<QRectF,JKQTMathTextTBRDataH<QString>,JKQTPDataCacheThreadSafe,JKQTMathTextTBRDataHExt<QString>> cache(
[](const JKQTMathTextTBRDataHExt<QString>& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.tightBoundingRect(key.text);
Expand All @@ -986,7 +1031,7 @@ QRectF JKQTMathTextGetTightBoundingRect(const QFont &f, const QString &text, QPa
QRectF JKQTMathTextGetBoundingRect(const QFont &f, const QString &text, QPaintDevice *pd)
{
//thread_local JKQTPDataCache<QRectF,JKQTMathTextTBRDataH,false,JKQTMathTextTBRDataHExt> cache(
static JKQTPDataCache<QRectF,JKQTMathTextTBRDataH<QString>,true,JKQTMathTextTBRDataHExt<QString>> cache(
static JKQTPDataCache<QRectF,JKQTMathTextTBRDataH<QString>,JKQTPDataCacheThreadSafe,JKQTMathTextTBRDataHExt<QString>> cache(
[](const JKQTMathTextTBRDataHExt<QString>& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.boundingRect(key.text);
Expand All @@ -999,7 +1044,7 @@ QRectF JKQTMathTextGetBoundingRect(const QFont &f, const QString &text, QPaintDe
qreal JKQTMathTextGetHorAdvance(const QFont &f, const QString &text, QPaintDevice *pd)
{
//thread_local JKQTPDataCache<double,JKQTMathTextTBRDataH,false,JKQTMathTextTBRDataHExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextTBRDataH<QString>,true,JKQTMathTextTBRDataHExt<QString>> cache(
static JKQTPDataCache<qreal,JKQTMathTextTBRDataH<QString>,JKQTPDataCacheThreadSafe,JKQTMathTextTBRDataHExt<QString>> cache(
[](const JKQTMathTextTBRDataHExt<QString>& key) {
const QFontMetricsF fm(key.f, key.pd);
#if (QT_VERSION>=QT_VERSION_CHECK(5, 15, 0))
Expand All @@ -1016,7 +1061,7 @@ qreal JKQTMathTextGetHorAdvance(const QFont &f, const QString &text, QPaintDevic
qreal JKQTMathTextGetRightBearing(const QFont &f, const QChar &text, QPaintDevice *pd)
{
//thread_local JKQTPDataCache<double,JKQTMathTextTBRDataH,false,JKQTMathTextTBRDataHExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextTBRDataH<QChar>,true,JKQTMathTextTBRDataHExt<QChar>> cache(
static JKQTPDataCache<qreal,JKQTMathTextTBRDataH<QChar>,JKQTPDataCacheThreadSafe,JKQTMathTextTBRDataHExt<QChar>> cache(
[](const JKQTMathTextTBRDataHExt<QChar>& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.rightBearing(key.text);
Expand All @@ -1028,7 +1073,7 @@ qreal JKQTMathTextGetRightBearing(const QFont &f, const QChar &text, QPaintDevic
qreal JKQTMathTextGetLeftBearing(const QFont &f, const QChar &text, QPaintDevice *pd)
{
//thread_local JKQTPDataCache<double,JKQTMathTextTBRDataH,false,JKQTMathTextTBRDataHExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextTBRDataH<QChar>,true,JKQTMathTextTBRDataHExt<QChar>> cache(
static JKQTPDataCache<qreal,JKQTMathTextTBRDataH<QChar>,JKQTPDataCacheThreadSafe,JKQTMathTextTBRDataHExt<QChar>> cache(
[](const JKQTMathTextTBRDataHExt<QChar>& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.leftBearing(key.text);
Expand All @@ -1040,7 +1085,7 @@ qreal JKQTMathTextGetLeftBearing(const QFont &f, const QChar &text, QPaintDevice

qreal JKQTMathTextGetFontAscent(const QFont &f, QPaintDevice *pd)
{
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,true,JKQTMathTextCacheKeyBaseExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,JKQTPDataCacheThreadSafe,JKQTMathTextCacheKeyBaseExt> cache(
[](const JKQTMathTextCacheKeyBaseExt& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.ascent();
Expand All @@ -1051,7 +1096,7 @@ qreal JKQTMathTextGetFontAscent(const QFont &f, QPaintDevice *pd)

qreal JKQTMathTextGetFontDescent(const QFont &f, QPaintDevice *pd)
{
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,true,JKQTMathTextCacheKeyBaseExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,JKQTPDataCacheThreadSafe,JKQTMathTextCacheKeyBaseExt> cache(
[](const JKQTMathTextCacheKeyBaseExt& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.descent();
Expand All @@ -1062,7 +1107,7 @@ qreal JKQTMathTextGetFontDescent(const QFont &f, QPaintDevice *pd)

qreal JKQTMathTextGetFontHeight(const QFont &f, QPaintDevice *pd)
{
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,true,JKQTMathTextCacheKeyBaseExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,JKQTPDataCacheThreadSafe,JKQTMathTextCacheKeyBaseExt> cache(
[](const JKQTMathTextCacheKeyBaseExt& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.height();
Expand All @@ -1073,7 +1118,7 @@ qreal JKQTMathTextGetFontHeight(const QFont &f, QPaintDevice *pd)

qreal JKQTMathTextGetFontLineWidth(const QFont &f, QPaintDevice *pd)
{
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,true,JKQTMathTextCacheKeyBaseExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,JKQTPDataCacheThreadSafe,JKQTMathTextCacheKeyBaseExt> cache(
[](const JKQTMathTextCacheKeyBaseExt& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.lineWidth();
Expand All @@ -1084,7 +1129,7 @@ qreal JKQTMathTextGetFontLineWidth(const QFont &f, QPaintDevice *pd)

qreal JKQTMathTextGetFontLeading(const QFont &f, QPaintDevice *pd)
{
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,true,JKQTMathTextCacheKeyBaseExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,JKQTPDataCacheThreadSafe,JKQTMathTextCacheKeyBaseExt> cache(
[](const JKQTMathTextCacheKeyBaseExt& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.leading();
Expand All @@ -1095,7 +1140,7 @@ qreal JKQTMathTextGetFontLeading(const QFont &f, QPaintDevice *pd)

qreal JKQTMathTextGetFontLineSpacing(const QFont &f, QPaintDevice *pd)
{
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,true,JKQTMathTextCacheKeyBaseExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,JKQTPDataCacheThreadSafe,JKQTMathTextCacheKeyBaseExt> cache(
[](const JKQTMathTextCacheKeyBaseExt& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.lineSpacing();
Expand All @@ -1106,7 +1151,7 @@ qreal JKQTMathTextGetFontLineSpacing(const QFont &f, QPaintDevice *pd)

qreal JKQTMathTextGetFontStrikoutPos(const QFont &f, QPaintDevice *pd)
{
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,true,JKQTMathTextCacheKeyBaseExt> cache(
static JKQTPDataCache<qreal,JKQTMathTextCacheKeyBase,JKQTPDataCacheThreadSafe,JKQTMathTextCacheKeyBaseExt> cache(
[](const JKQTMathTextCacheKeyBaseExt& key) {
const QFontMetricsF fm(key.f, key.pd);
return fm.strikeOutPos();
Expand Down
Binary file modified screenshots/multithreaded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/multithreaded_small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 1bf6c43

Please sign in to comment.