From e8d362fb04feb96e9477f71647365800481146b5 Mon Sep 17 00:00:00 2001 From: Aria Shrimpton Date: Wed, 6 Mar 2024 14:18:45 +0000 Subject: work on implementation --- thesis/parts/implementation.tex | 69 ++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 21 deletions(-) (limited to 'thesis') diff --git a/thesis/parts/implementation.tex b/thesis/parts/implementation.tex index 5009836..3576447 100644 --- a/thesis/parts/implementation.tex +++ b/thesis/parts/implementation.tex @@ -3,50 +3,77 @@ \section{Modifications to Primrose} %% API +In order to facilitate integration with Primrose, we refactored large parts of the code to support being called as an API, rather than only through the command line. +This also required updating the older code to a newer edition of Rust, and improving the error handling throughout. %% Mapping trait +As suggested in the original paper, we added the ability to ask for associative container types: ones that map a key to a value. +This was done by adding a new \code{Mapping} trait to the library, and updating the type checking and analysis code to support multiple type variables in container type declarations, and be aware of the operations available on mappings. -%% We add a new mapping trait to primrose to express KV maps +Operations on mapping implementations can be modelled and checked against constraints in the same way that regular containers can be. +They are modelled in Rosette as a list of key-value pairs. +\code{src/crates/library/src/hashmap.rs} shows how mapping container types can be declared, and operations on them modelled. -%% \todo{add and list library types} +\todo{add and list library types} -%% the constraint solver has been updated to allow properties on dicts (dictproperty), but this was unused. +We also added new syntax to the language to support defining properties that only make sense for mappings (\code{dictProperty}), however this was unused. %% Resiliency, etc +While performing integration testing, we found and fixed several other issues with the existing code: -\section{Cost models} +\begin{enumerate} +\item Only push and pop operations could be modelled in properties without raising an error during type-checking. +\item The Rosette code generated for properties using other operations would be incorrect. +\item Some trait methods used mutable borrows unnecessarily, making it difficult or impossible to write safe Rust using them. +\item The generated code would perform an unnecessary heap allocation for every created container, which could affect performance. +\end{enumerate} + +We also added a requirement for all \code{Container}s and \code{Mappings} to implement \code{IntoIterator} and \code{FromIterator}, as well as to allow iterating over elements. + +\section{Building cost models} %% Benchmarker crate +In order to benchmark container types, we use a seperate crate (\code{src/crates/candelabra-benchmarker}) which contains benchmarking code for each trait in the Primrose library. +When benchmarks need to be run for an implementation, we dynamically generate a new crate, which runs all benchmark methods appropriate for the given implementation (\code{src/crate/candelabra/src/cost/benchmark.rs}). + +As Rust's generics are monomorphised, our generic code is compiled as if we were using the concrete type in our code, so we don't need to worry about affecting the benchmark results. -%% each trait has its own set of benchmarks, which run different workloads -%% benchmarker trait doesn't have Ns -%% example benchmarks for hashmap and vec +Each benchmark is run in a 'warmup' loop for a fixed amount of time (currently 500ms), then runs for a fixed number of iterations (currently 50). +This is important because we use every observation when fitting our cost models, so varying our number of iterations would change our curve's fit. +We repeat each benchmark at a range of $n$ values, ranging from $64$ to $65,536$. -%% Code generation +Each benchmark we run corresponds to one container operation. +For most operations, we prepare a container of size $n$ and run the operation once per iteration. +For certain operations which are commonly amortized (\code{insert}, \code{push}, and \code{pop}), we instead run the operation itself $n$ times and divide all data points by $n$. -%% \code{candelabra::cost::benchmark} generates code which just calls \code{candelabra\_benchmarker} methods -%% Ns are set there, and vary from [...] +Our benchmarker crate outputs every observation in a similar format to Criterion (a popular benchmarking crate for Rust). +We then parse this from our main program, and use least squares to fit a polynomial to our data. +We initially tried other approaches to fitting a curve to our data, however we found that they all overfitted, resulting in more sensitivity to benchmarking noise. +As operations on most common data structures are polynomial or logarithmic complexity, we believe that least squares fitting is good enough to capture the cost of most operations. -%% fitting done with least squares in \code{candelabra::cost::fit} -%% list other methods tried -%% simple, which helps 'smooth out' noisy benchmark results +\todo{variable coefficients, which ones we tried} \section{Profiling} -%% profiler type in \code{primrose\_library::profiler} -%% wraps an 'inner' implementation and implements whatever operations it does, keeping track of number of calls -%% on drop, creates new file in folder specified by env variable +We implement profiling by using a \code{ProfilerWrapper} type (\code{src/crates/library/src/profiler.rs}), which takes as a type parameter the 'inner' container implementation. +We then implement any primrose traits that the inner container implements, counting the number of times each operation is called. +We also check the length of the container after each insertion operation, and track the maximum. -%% primrose results generated in \code{primrose::codegen}, which is called in \code{candelabra::profiler} -%% picks the first valid candidate - performance doesn't really matter for this case -%% each drop generates a file, so we get details of every individual collection allocated +This tracking is done per-instance, and recorded when the instance goes out of scope and its \code{Drop} implementation is called. +We write the counts of each operation and maximum size of the collection to a location specified by an environment variable, and a constant generic parameter which allows us to match up container types to their profiler outputs. -\section{Selection and Codegen} +When we want to profile a program, we pick any valid inner implementation for each selection site, and use that candidate with our profiling wrapper as the concrete implementation for that site. -%% Generated code (opaque types) +This approach has the advantage of giving us information on each individual collection allocated, rather than only statistics for the type as a whole. +For example, if one instance of a container type is used in a very different way from the rest, we will be able to see it more clearly than a normal profiling tool would allow us to. +Although it has some amount of overhead, it's not important as we aren't measuring the program's execution time when profiling. + +\section{Selection and Codegen} %% Selection Algorithm incl Adaptiv +%% Generated code (opaque types) + %% Implementation w/ const generics \section{Misc Concerns} -- cgit v1.2.3