From 67cc263438451f8688cf7e599dbfd93e4b3300ae Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 16 Oct 2023 17:18:35 +0100 Subject: more redrafting --- thesis/parts/background.tex | 68 ++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 26 deletions(-) (limited to 'thesis/parts') diff --git a/thesis/parts/background.tex b/thesis/parts/background.tex index 633efde..7b995b0 100644 --- a/thesis/parts/background.tex +++ b/thesis/parts/background.tex @@ -6,10 +6,10 @@ Finally, we examine the gaps in the existing literature, and explain how this pa The vast majority of programs will use make extensive use of collection data types - types intended to hold many different instances of other data types. This can refer to anything from fixed-size arrays, to growable linked lists, to associative key-value mappings or dictionaries. -In some cases, these are built-in parts of the language: In Go, a list of ints has type \code{[]int} and a dictionary from string to string has type \code{map[string]string}. +In some cases, these are built-in parts of the language: In Go, a list of ints has type \code{[]int} and a map from string to string has type \code{map[string]string}. -In other languages, these are instead part of some standard library. -In Rust, you might write \code{Vec} and \code{HashMap} for the same types. +In other languages, these are instead part of some standard library, or in some cases must be defined by the user. +In Rust, you might write \code{Vec} and \code{HashMap} for the same purpose. This forces us to make a choice upfront: what type should we use? In this case the answer is obvious - the two have very different purposes and don't support the same operations. @@ -23,61 +23,84 @@ We refer to this problem as container selection, and split it into two parts: Fu Functional requirements refers to a similar definition as is normally used for software: The container must behave the way that the program expects it to. -Continuing with our previous example, we can first note that \code{Vec} and \code{HashSet} implement different sets of methods. +Continuing with our previous example, we can see that \code{Vec} and \code{HashSet} implement different methods. \code{Vec} implements methods like \code{.get(index)} and \code{.push(value)}, while \code{HashSet} implements neither - they don't make sense for an unordered collection. Similarly, \code{HashSet} implements \code{.replace(value)} and \code{.is\_subset(other)}, neither of which make sense for \code{Vec}. -If we try to swap \code{Vec} for \code{HashSet}, the resulting program may not compile. +If we try to swap \code{Vec} for \code{HashSet}, the resulting program will likely not compile. These restrictions form the first part of our functional requirements - the ``syntactic properties'' of the containers must satisfy the program's requirements. In object-oriented programming, we might say they must implement an interface. +In Rust, we would say that they implement a trait, or that they belong to a type class. However, syntactic properties alone are not always enough to select an appropriate container. Suppose our program only requires a container to have \code{.insert(value)}, \code{.contains(value)}, and \code{.len()}. Both \code{Vec} and \code{HashSet} will satisfy these requirements. - However, our program might rely on \code{.len()} returning a count including duplicates. In this case, \code{HashSet} would give us different behaviour, possibly causing our program to behave incorrectly. To express this, we say that a container implementation also has ``semantic properties'' that must satisfy our requirements. Intuitively we can think of this as what conditions the container upholds. -For a set, this would include that there are never any duplicates % TODO +For a \code{HashSet}, this would include that there are never any duplicates, whereas for a Vec it would include that ordering is preserved. \subsection{Non-functional requirements} While meeting the functional requirements is generally enough to ensure a program runs correctly, we also want to ensure we choose the 'best' type we can. -There are many measures for this, but we will focus primarily on time: how much we can affect the runtime of the program. +For our purposes, this will simply be the type that minimises runtime, although other approaches also consider the balance between memory usage and time. -If we assume we can find a selection of types that satisfy the functional requirements, then one obvious solution is just to benchmark the program with each of these implementations in place, and see which works best. +Prior work has shown that properly considering container selection selection can give substantial performance improvements, even in large applications. +For instance, tuning performed in \cite{chung_towards_2004} achieved an up to 70\% increase in the throughput of a complex web application, and a 15-40\% decrease in the runtime of several scientific applications. +\cite{l_liu_perflint_2009} found and suggested fixes for ``hundreds of suboptimal patterns in a set of large C++ benchmarks'', with one such case improving performance by 17\%. +Similarly, \cite{jung_brainy_2011} achieves an average speedup of 27-33\% on real-world applications and libraries. -This will obviously work, however note that as well as our program, we need to develop benchmarks. -If the benchmarks are flawed, or don't represent how our program is used in practice, then we may get drastically different results in the 'real world'. +If we assume we can find a selection of types that satisfy the functional requirements, then one obvious solution is just to benchmark the program with each of these implementations in place, and see which works best. +This will obviously work, so long as our benchmarks are roughly representative of 'real world' inputs. -%% TODO: Motivate how this improves performance +Unfortunately, this technique scales poorly for bigger applications. +As the number of container types we must select increases, the number of combinations we must try increases exponentially (assuming they all have roughly the same number of candidates). +This quickly becomes unfeasible, and so we must find other ways of improving our performance. \section{Prior Literature} \subsection{Approaches in common programming languages} -%% TODO +Modern programming languages broadly take one of two approaches to container selection. + +Some languages, usually higher-level ones, recommend built-in structures as the default, using implementations that perform fine for the vast majority of use-cases. +Popular examples include Python, which uses \code{[1, 2, 3]} and \code{\{'one': 1\}} for lists and maps respectively; and Go, which uses \code{int[]\{1, 2, 3\}} and \code{map[string]int\{"one": 1\}} for the same purposes. +This approach prioritises developer ergonomics: programmers writing in these languages do not need to think about how these are implemented in the vast majority of cases. +In both languages, other implementations are possible to a certain extent, although these aren't usually preferred and come at the cost of code readability. + +In other languages, collections are given as part of a standard library, or must be written by the user. +For example, C does not support growable lists at the language level - users must bring in their own implementation or use an existing library. +Java comes with growable lists and maps as part of its standard library, as does Rust (with some macros to make use easier). +In both cases, the ``blessed'' implementation of collections is not special - users can implement their own. + +In many languages, interfaces or their closest equivalent are used to distinguish 'similar' collections. +In Java, ordered collections implement the interface \code{List}, while similar interfaces exist for \code{Set}, \code{Queue}, etc. +This means that when the developer chooses a type, the compiler enforces the syntactic requirements of the collection, and the writer of the implementaiton ``promises'' they have met the semantic requirements. +Other languages give much weaker guarantees, for instance Rust has no typeclasses for List or Set. +Its closest equivalents are traits like \code{Index} and \code{IntoIterator}, neither of which make semantic guarantees. + +Whilst the approach Java takes is the most expressive, both of these approaches either put the choice on the developer, or remove the choice entirely. +This means that developers are forced to guess based on their knowledge of the underlying implementations, or more often to just pick the most common implementation. +The papers we will examine all attempt to choose for the developer, based on a variety of techniques. \subsection{Chameleon} -Chameleon\parencite{shacham_chameleon_2009} is a solution that focuses on the non-functional requirements of container selection. +Chameleon\parencite{shacham_chameleon_2009} is a tool for Java codebases, which uses a rules engine to identify sub-optimal choices. -First, it runs the program with some example input, and collects data on the collections used using a ``semantic profiler''. -This data includes the space used by collections, the minimum space that could be used by all of the items of that collection, and the number of each operation performed. +First, it runs the program with some representative input, and collects data on the collections used using a ``semantic profiler''. +This data includes the space used by collections, the minimum space that could be used by all of the items of that collection, and the counts of each operation performed. These statistics are tracked per individual collection allocated, and then aggregated by 'allocation context' - a portion of the callstack where the allocation occured. These aggregated statistics are then passed to a rules engine, which uses a set of rules to suggest places a different container type might improve performance. For example, a rule could check when a linked list often has items accessed by index, and suggest a different list implementation as a replacement. This results in a flexible engine for providing suggestions, which can be extended with new rules and types as necessary. -%% todo: something about online selection part - Unfortunately, this does require the developer to come up with and add replacement rules for each implementation. In many cases, there may be patterns that could be used to suggest a better option, but that the developer does not see or is not able to formalise. -Chameleon also makes no attempt to select based on functional requirements. +Chameleon also relies only on the existing type to decide what it can suggest. This results in selection rules needing to be more restricted than they otherwise could be. For instance, a rule cannot suggest a \code{HashSet} instead of a \code{LinkedList}, as the two are not semantically identical. Chameleon has no way of knowing if doing so will break the program's functionality, and so it does not make a suggestion. @@ -114,13 +137,6 @@ However, this approach is still limited in the semantics it can identify, for in \subsection{CollectionSwitch} -%% - online selection - uses library so easier to integrate -%% - collects access patterns, size patterns, etc. -%% - performance model is built beforehand for each concrete implementation, with a cost model used to estimate the relative performance of each based on observed usage -%% - switches underlying implementation dynamically -%% - also able to decide size thresholds where the implementation should be changed and do this -%% - doesn't require specific knowledge of the implementations, although does still assume all are semantically equivalent - CollectionSwitch\parencite{costa_collectionswitch_2018} takes a different approach to the container selection problem, adapting as the program runs and new information becomes available. First, a performance model is built for each container implementation. -- cgit v1.2.3