SwarmSolver: cache cell lookups and individual fitness

Two changes that cut the cost of the search, with the same result.

getCell resolved a cell from scratch on every read and write: fetch
the sheets, pick the sheet, query interfaces, then ask for the cell.
The search touches the same few cells many thousands of times. Keep
the resolved cells in a small cache keyed by sheet, column and row,
cleared at the start of each solve.

Differential Evolution recomputed the fitness of the original
candidate on every comparison, and each fitness writes every variable
into the document and reads the objective back. Store the fitness on
the individual when it is created, then reuse the stored value. This
roughly halves the number of fitness evaluations per generation.

On a four variable integer model the whole run dropped from about 16
to about 11 seconds, most of the remainder being fixed start up cost.

Signed-off-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.co.uk>
Change-Id: I96ea8c324c194dc217199f0b5318782937d43b92
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/4057
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
(cherry picked from commit 7c97a43dc1b0127da468dd92ac0e7823e8788086)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/208442
Tested-by: Jenkins
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
Tomaž Vajngerl
2026-07-22 21:52:37 +02:00
committed by Xisco Fauli
parent f4c5255cd6
commit acd586803b
2 changed files with 52 additions and 3 deletions
+10 -2
View File
@@ -17,6 +17,10 @@
struct Individual
{
std::vector<double> mVariables;
// Fitness of mVariables, kept so it does not have to be recomputed. Each
// recompute writes every variable into the document and reads the
// objective back, which is by far the most expensive part of the search.
double mFitness = std::numeric_limits<double>::lowest();
};
template <typename DataProvider> class DifferentialEvolutionAlgorithm
@@ -79,6 +83,7 @@ public:
maPopulation.emplace_back();
Individual& rIndividual = maPopulation.back();
mrDataProvider.initializeVariables(rIndividual.mVariables, maGenerator);
rIndividual.mFitness = mrDataProvider.calculateFitness(rIndividual.mVariables);
}
}
@@ -135,9 +140,12 @@ public:
double fCandidateFitness = mrDataProvider.calculateFitness(aCandidate.mVariables);
// see if is better than original, if so replace
if (fCandidateFitness > mrDataProvider.calculateFitness(rOriginal.mVariables))
// Replace the original if the candidate is better. The original's
// fitness was computed when it was created, so reuse the stored
// value here.
if (fCandidateFitness > rOriginal.mFitness)
{
aCandidate.mFitness = fCandidateFitness;
maPopulation[x] = std::move(aCandidate);
if (fCandidateFitness > mfBestFitness)
+42 -1
View File
@@ -34,6 +34,9 @@
#include <limits>
#include <chrono>
#include <random>
#include <unordered_map>
#include <o3tl/hash_combine.hxx>
#include <unotools/resmgr.hxx>
#include <comphelper/servicehelper.hxx>
@@ -91,6 +94,31 @@ struct Bound
}
};
// A cell position used as a cache key.
struct CellKey
{
sal_Int32 nSheet;
sal_Int32 nColumn;
sal_Int32 nRow;
bool operator==(const CellKey& rOther) const
{
return nSheet == rOther.nSheet && nColumn == rOther.nColumn && nRow == rOther.nRow;
}
};
struct CellKeyHash
{
size_t operator()(const CellKey& rKey) const
{
size_t nSeed = 0;
o3tl::hash_combine(nSeed, rKey.nSheet);
o3tl::hash_combine(nSeed, rKey.nColumn);
o3tl::hash_combine(nSeed, rKey.nRow);
return nSeed;
}
};
enum
{
PROP_NONNEGATIVE,
@@ -133,6 +161,11 @@ private:
std::vector<Bound> maBounds;
std::vector<sheet::SolverConstraint> maNonBoundedConstraints;
// Resolved cells, keyed by sheet, column and row. The search reads and
// writes the same handful of cells many thousands of times, so resolving
// each one only once is a large saving.
std::unordered_map<CellKey, uno::Reference<table::XCell>, CellKeyHash> maCellCache;
private:
static OUString getResourceString(TranslateId aId);
@@ -294,10 +327,17 @@ OUString SwarmSolver::getResourceString(TranslateId aId)
uno::Reference<table::XCell> SwarmSolver::getCell(const table::CellAddress& rPosition)
{
CellKey aKey{ rPosition.Sheet, rPosition.Column, rPosition.Row };
auto aFound = maCellCache.find(aKey);
if (aFound != maCellCache.end())
return aFound->second;
uno::Reference<container::XIndexAccess> xSheets(mxDocument->getSheets(), uno::UNO_QUERY);
uno::Reference<sheet::XSpreadsheet> xSheet(xSheets->getByIndex(rPosition.Sheet),
uno::UNO_QUERY);
return xSheet->getCellByPosition(rPosition.Column, rPosition.Row);
uno::Reference<table::XCell> xCell = xSheet->getCellByPosition(rPosition.Column, rPosition.Row);
maCellCache.emplace(aKey, xCell);
return xCell;
}
// Build a document cell address from the API address, throwing if its sheet is
@@ -584,6 +624,7 @@ void SAL_CALL SwarmSolver::solve()
// are only ever appended.
maBounds.assign(maVariables.getLength(), Bound());
maNonBoundedConstraints.clear();
maCellCache.clear();
xModel->lockControllers();