aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAria Shrimpton <me@aria.rip>2024-02-23 12:27:20 +0000
committerAria Shrimpton <me@aria.rip>2024-02-23 12:42:07 +0000
commitf1a9c9a10ef2c216532536b26fd1edc92949a219 (patch)
treef2c9edb87ec12b7232075454cd41d5edd185c2bd
parentd3be4136218ac50f4f46ff739f858300e5313a85 (diff)
some design writing
-rw-r--r--thesis/parts/design.tex44
1 files changed, 29 insertions, 15 deletions
diff --git a/thesis/parts/design.tex b/thesis/parts/design.tex
index 1ff0048..80c208c 100644
--- a/thesis/parts/design.tex
+++ b/thesis/parts/design.tex
@@ -44,19 +44,28 @@ Once we've specified our functional requirements and provided a benchmark (\code
\todo{Show selection process}
+\section{Overview of process}
+
Our tool integrates with Rust's packaging system (Cargo) to discover the information it needs about our project, then runs Primrose to find a list of implementations satsifying our functional requirements.
-Once it has this list, it estimates a 'cost' for each candidate, which is an upper bound on the total time taken for all container operations.
-At this point, it also checks if an ``adaptive'' container would be better, by checking if one implementation is better performing at a lower n, and another at a higher n.
+Once we have this list, we then build a 'cost model' for each candidate type. This allows us to get an upper bound for the runtime cost of an operation at any given n.
+
+We then run the user-provided benchmarks, using any of the valid candidates instrumented to track the number of each operation, and the maximum n value it reaches.
+
+We combine this information with our cost models to estimate a total cost for each candidate, which is an upper bound on the total time taken for all container operations.
+At this point, we also check if an 'adaptive' container would be better, by checking if one implementation is better performing at a lower n, and another at a higher n.
+
+Finally, we pick the container with the minimum cost, and create a new Rust file where the chosen container type is exported.
-Finally, it picks the container with the minimum cost, and creates a new Rust file where the chosen container type is exported.
+Our solution requires little user intervention, integrates well with existing workflows, and the time it takes scales linearly with the number of container types in a given project.
-Our tool requires little user intervention, integrates well with existing workflows, and the time it takes scales linearly with the number of container types in a given project.
+We now go into more detail on how each step works, although we leave some specifics until Chapter \ref{chap:implementation}.
\section{Functional requirements}
%% Explain role in entire process
As described in Chapter \ref{chap:background}, any implementation we pick needs to satisfy the program's functional requirements. We do this by integrating Primrose \parencite{qin_primrose_2023} as a first step.
+
Primrose allows users to specify both the traits they require in an implementation (essentially the API and methods available), and what properties must be satisfied.
Each container type that we want to select an implementation for is bound by a list of traits and a list of properties (lines 11 and 12 in Listing \ref{lst:selection_example}).
@@ -71,9 +80,6 @@ In brief, primrose works by:
We use the code provided with the Primrose paper, with minor modifications elaborated on in Chapter \ref{chap:implementation}.
-%% Abstraction over backend
-Although we use primrose in our implementation, the rest of our system isn't dependent on it, and it would be relatively simple to use a different approach to select based on functional requirements.
-
At this stage, we have a list of implementations for each container type we are selecting. The command \code{candelabra-cli candidates} will show this output, as in Table \ref{table:candidates_prime_sieve}.
\begin{table}[h]
@@ -91,6 +97,9 @@ At this stage, we have a list of implementations for each container type we are
\label{table:candidates_prime_sieve}
\end{table}
+%% Abstraction over backend
+Although we use primrose in our implementation, the rest of our system isn't dependent on it, and it would be relatively simple to use a different approach to select based on functional requirements.
+
\section{Cost Models}
We use an approach similar to CollectionSwitch\parencite{costa_collectionswitch_2018}, which assumes that the main factor in how long an operation takes is the current size of the collection.
@@ -115,21 +124,20 @@ For example, the container implementation \code{LazySortedVec} (provided by Prim
We were unable to work around this, and so we have removed these variants from our container library.
A potential solution could be to perform untimed 'warmup' operations before each operation, but this is complex because it requires some understanding of what operations will cause work to be deferred.
-\todo{Find a place for this}
-Whilst it would be possible to share this data across computers, micro-architecture can have a large effect on collection performance\parencite{jung_brainy_2011}, so we calculate it on demand.
-
\section{Profiling applications}
%% Data Collected
As mentioned above, the ordering of operations can have a large effect on container performance.
Unfortunately, tracking every container operation in order quickly becomes unfeasible, so we settle for tracking the count of each operation, and the maximum size of each collection instance.
-Profiling is done per-instance, so for every place a new container is allocated.
-Although we collect these seperately, we immediately summarise them into a list of partitions.
-
+Every instance/allocation of the collection is tracked separately, and results are collated after profiling.
%% Segmentation
-Results are grouped by their n value - everything with a close enough n value gets put in the same partition, where we average our metrics.
-This provides a form of compression, and is also used to detect situations where an adaptive container should be used (see next section).
+Results with a close enough n value get sorted into partitions, where each partition stores the average count of each operation, and a weight indicating how common results in that partition were.
+This serves 3 purposes.
+
+The first is to compress the data, which speeds up processing and stops us running out of memory in more complex programs.
+The second is to capture the fact that the number of operations will likely depend on the size of the container.
+The third is to aid in searching for adaptive containers, which will be elaborated on later.
%% Limitations w/ pre-benchmark steps
\todo{not taking into account 'preparatory' operations during benchmarks}
@@ -137,6 +145,12 @@ This provides a form of compression, and is also used to detect situations where
\section{Selection process \& adaptive containers}
%% Selection process
+Once we have an estimate of how long each operation may take (from our cost models), and how often we use each operation (from our profiling information), we combine these to estimate the total cost of each implementation.
+For each implementation, our total cost estimate is:
+
+\[ \sum_{op\in \textrm{ops}} \sum_{(r_{op}, N) \in \textrm{partitions}} C_\textrm{op}(N) * r_\textrm{op} \]
+
+where $C_{op}$ is the cost estimated by the cost model for operation $op$ at n value $N$, and $r_{op}, N$ is the average count of a given operation and the maximum N in a partition.
%% Adaptive container detection