diff options
Diffstat (limited to 'doc/Bonus/tuple_vector_readme.md')
-rw-r--r-- | doc/Bonus/tuple_vector_readme.md | 416 |
1 files changed, 0 insertions, 416 deletions
diff --git a/doc/Bonus/tuple_vector_readme.md b/doc/Bonus/tuple_vector_readme.md deleted file mode 100644 index f406ac5..0000000 --- a/doc/Bonus/tuple_vector_readme.md +++ /dev/null @@ -1,416 +0,0 @@ -## Introduction to tuple_vector - -`tuple_vector` is a data container that is designed to abstract and simplify -the handling of a "structure of arrays" layout of data in memory. In -particular, it mimics the interface of `vector`, including functionality to do -inserts, erases, push_backs, and random-access. It also provides a -`RandomAccessIterator` and corresponding functionality, making it compatible -with most STL (and STL-esque) algorithms such as ranged-for loops, `find_if`, -`remove_if`, or `sort`. - -When used or applied properly, this container can improve performance of -some algorithms through cache-coherent data accesses or allowing for -sensible SIMD programming, while keeping the structure of a single -container, to permit a developer to continue to use existing algorithms in -STL and the like. - -## Review of "Structure of arrays" data layouts - -When trying to improve the performance of some code, it can sometimes be -desirable to transform how some data is stored in memory to be laid out not as -an "array of structures", but as a "structure of arrays". That is, instead of -storing a series of objects as a single contiguous chunk of memory, one or -more data members are instead stored as separate chunks of memory that are -handled and accessed in parallel to each other. - -This can be beneficial in two primary respects: - -1) To improve the cache coherency of the data accesses, e.g. by utilizing more -data that is loaded per cache line loaded from memory, and thereby reducing -the amount of time waiting on memory accesses from off-CPU memory. -This presentation from Mike Acton touches on this, among other things: -https://www.youtube.com/watch?v=rX0ItVEVjHc - -2) To allow the data to be more easily loaded and utilized by SIMD kernels, -by being able to load memory directly into a SIMD register. -This is touched on in this presentation from Andreas Fredriksson for writing -code with SIMD intrinsics: -http://www.gdcvault.com/play/1022249/SIMD-at-Insomniac-Games-How -...and as well in this guide for writing performant ISPC kernels: -https://ispc.github.io/perfguide.html - -## How TupleVecImpl works - -`tuple_vector` inherits from `TupleVecImpl`, which -provides the bulk of the functionality for those data containers. It manages -the memory allocated, marshals data members to each array of memory, generates -the necessary iterators, and so on. - -When a `tuple_vector` is declared, it is alongside a list of types, or "tuple -elements", indicating what data to store in the container, similar to how `tuple` -operates. `TupleVecImpl` uses this list of tuple elements to then inherit from a series of -`TupleVecLeaf` structures, which each have their own pointer to an array of their -corresponding type in memory. When dereferencing the container, either to fetch a -tuple of references or just fetching pointers to the memory, it is these pointers -that are utilized or fetched. - -While each `TupleVecLeaf` contains a pointer to its own block of memory, they -are not individual memory allocations. When `TupleVecImpl` needs to grow its -capacity, it calculates the total size needed for a single allocation, taking -into account the number of objects for the container, the size of each tuple -element's type, and the alignment requirements for each type. Pointers into the -allocation for each tuple element are also determined at the same time, which -are passed to each `TupleVecLeaf`. From there, many of the interactions with -`TupleVecImpl`, to modify or access members of the container, then reference -each `TupleVecLeaf`'s data pointer in series, using parameter packs to repeat -each operation for each parent `TupleVecLeaf`. - -## How tuple_vector's iterator works - -`TupleVecImpl` provides a definition to an iterator type, `TupleVecIter`. -As mentioned above, `TupleVecIter` provides all of the functionality to operate -as a `RandomAccessIterator`. When it is dereferenced, it provides a tuple of -references, similar to `at()` or `operator[]` on `TupleVecImpl`, as opposed to -a reference of some other type. As well, a customization of `move_iterator` for -`TupleVecIter` is provided, which will return a tuple of rvalue-references. - -The way that `TupleVecIter` operates internally is to track an index into the -container, as well as a copy of all of the `TupleVecImpl`'s `TupleVecLeaf` -pointers at the time of the iterator's construction. As a result, modifying the -iterator involves just changing the index, and dereferencing the iterator into -the tuple of references involves dereferencing each pointer with an offset -specified by that index. - -Of the various ways of handling the multitude of references, this tended to -provide the best code-generation. For example, having a tuple of pointers that -are collectively modified with each iterator modification resulted in the compiler -not being able to accurately determine which pointers were relevant to the final -output of some function, creating many redundant operations. Similarly, having -the iterator refer to the source `TupleVecImpl` for the series of pointers -often resulted in extra, unnecessary, data hops to the `TupleVecImpl` to repeatedly -fetch data that was not practically mutable, but theoretically mutable. While this -solution is the heaviest in terms of storage, the resulted assembly tends to be -competitive with traditional structure-of-arrays setups. - -## How to work with tuple_vector, and where to use it - -Put simply, `tuple_vector` can be used as a replacement for `vector`. For example, -instead of declaring a structure and vector as: - -``` -struct Entity -{ - bool active; - float lifetime; - Vec3 position; -} -vector<Entity> entityVec; -``` - -...the `tuple_vector` equivalent of this can be defined as: - -``` -tuple_vector<bool, float, Vec3> entityVec; -``` - -In terms of how `tuple_vector` is modified and accessed, it has a similar -featureset as `vector`, except where `vector` would accept or return a single -value, it instead accepts or returns a tuple of values or unstructured series -of equivalent arguments. - -For example, the following functions can be used to access the data, either by -fetching a tuple of references to a series of specific values, or the data -pointers to the tuple elements: - -``` -tuple<bool&, float&, Vec3&> operator[](size_type) -tuple<bool&, float&, Vec3&> at(size_type) -tuple<bool&, float&, Vec3&> iterator::operator*() -tuple<bool&&, float&&, Vec3&&> move_iterator::operator*() -tuple<bool*, float*, Vec3*> data() - -// extract the Ith tuple element pointer from the tuple_vector -template<size_type I> -T* get<I>() -// e.g. bool* get<0>(), float* get<1>(), and Vec3* get<2>() - -// extract the tuple element pointer of type T from the tuple_vector -// note that this function can only be used if there is one instance -// of type T in the tuple_vector's elements -template<typename T> -T* get<T>() -// e.g. bool* get<bool>(), float* get<float>(), and Vec3* get<Vec3>() -``` - -And `push_back(...)` has the following overloads, accepting either values or tuples as needed. - -``` -tuple<bool&, float&, Vec3&> push_back() -push_back(const bool&, const float&, const Vec3&) -push_back(tuple<const bool&, const float&,const Vec3&>) -push_back(bool&&, float&&, Vec3&&) -push_back(tuple<bool&&, float&&, Vec3&&>) -``` -...and so on, and so forth, for others like the constructor, `insert(...)`, -`emplace(...)`, `emplace_back(...)`, `assign(...)`, and `resize(...)`. - -As well, note that the tuple types that are accepted or returned for -`tuple_vector<Ts...>` have typedefs available in the case of not wanting to use -automatic type deduction: -``` -typedef eastl::tuple<Ts...> value_tuple; -typedef eastl::tuple<Ts&...> reference_tuple; -typedef eastl::tuple<const Ts&...> const_reference_tuple; -typedef eastl::tuple<Ts*...> ptr_tuple; -typedef eastl::tuple<const Ts*...> const_ptr_tuple; -typedef eastl::tuple<Ts&&...> rvalue_tuple; -``` -With this, and the fact that the iterator type satisfies -the `RandomAccessIterator` requirements, it is possible to use `tuple_vector` in -most ways and manners that `vector` was previously used, with few structural -differences. - -However, even if not using it strictly as a replacement for `vector`, it is -still useful as a tool for simplifying management of a traditional structure of -arrays. That is, it is possible to use `tuple_vector` to just perform a single -large memory allocation instead of a series of smaller memory allocations, -by sizing the `tuple_vector` as needed, fetching the necessary pointers with -`data()` or `get<...>()`, and carrying on normally. - -One example where this can be utilized is with ISPC integration. Given the -following ISPC function definition: - - export void simple(uniform float vin[], uniform float vfactors[], uniform float vout[], uniform int size); - -...which generates the following function prototype for C/C++ usage: - - extern void simple(float* vin, float* vfactors, float* vout, int32_t size); - -...this can be utilized with some raw float arrays: -``` -float* vin = new float[NumElements]; -float* vfactors = new float[NumElements]; -float* vout = new float[NumElements]; - -// Initialize input buffer -for (int i = 0; i < NumElements; ++i) -{ - vin[i] = (float)i; - vfactors[i] = (float)i / 2.0f; -} - -// Call simple() function from simple.ispc file -simple(vin, vfactors, vout, NumElements); - -delete vin; -delete vfactors; -delete vout; -``` -or, with `tuple_vector`: - -``` -tuple_vector<float, float, float> simpleData(NumElements); -float* vin = simpleData.get<0>(); -float* vfactors = simpleData.get<1>(); -float* vout = simpleData.get<2>(); - -// Initialize input buffer -for (int i = 0; i < NumElements; ++i) -{ - vin[i] = (float)i; - vfactors[i] = (float)i / 2.0f; -} - -// Call simple() function from simple.ispc file -simple(vin, vfactors, vout, NumElements); -``` - -`simpleData` here only has a single memory allocation during its construction, -instead of the three in the first example, and also automatically releases the -memory when it falls out of scope. - -It is possible to also skip a memory allocation entirely, in some circumstances. -EASTL provides "fixed" counterparts of many data containers which allows for a -data container to have an inlined buffer of memory. For example, -`eastl::vector<typename T>` has the following counterpart: - - eastl::fixed_vector<typename T, size_type nodeCount, bool enableOverflow = true> - -This buffer allows for enough space to hold a `nodeCount` number of `T` objects, -skipping any memory allocation at all, until the requested size becomes -greater than `nodeCount` - assuming `enableOverflow` is True. - -There is a similar counterpart to `eastl::tuple_vector<typename... Ts>` available as well: - - eastl::fixed_tuple_vector<size_type nodeCount, bool enableOverflow, typename... Ts> - -This does the similar legwork in creating an inlined buffer, and all of the -functionality of `tuple_vector` otherwise is supported. Note the slight -difference in declaration, though: `nodeCount` and `enableOverflow` are defined -first, and `enableOverflow` is not a default parameter. This change arises out -of restrictions surrounding variadic templates, in that they must be declared -last, and cannot be mixed with default template parameters. - -Lastly, `eastl::vector` and other EASTL data containers support custom Memory Allocator -types, through their template parameters. For example, `eastl::vector`'s full declaration -is actually: - - eastl::vector<typename T, typename AllocatorType = EASTLAllocatorType> - -However, because such a default template parameter cannot be used with -variadic templates, a separate type for `tuple_vector` is required for such a -definition: - - eastl::tuple_vector_alloc<typename AllocatorType, typename... Ts> - -Note that `tuple_vector` uses EASTLAllocatorType as the allocator. - -## Performance comparisons/discussion - -A small benchmark suite for `tuple_vector` is included when running the -EASTLBenchmarks project. It provides the following output on a Core i7 3770k -(Skylake) at 3.5GHz, with DDR3-1600 memory. - -The `tuple_vector` benchmark cases compare total execution time of similar -algorithms run against `eastl::tuple_vector` and `std::vector`, such as -erasing or inserting elements, iterating through the array to find a specific -element, sum all of the elements together via operator[] access, or just -running `eastl::sort` on the data containers. More information about the -EASTLBenchmarks suite can be found in EASTL/doc/EASTL Benchmarks.html - -Benchmark | STD execution time | EASTL execution time | Ratio ---------- | -------- | ---------- | ----- -`tuple_vector<AutoRefCount>/erase ` | 1.7 ms | 1.7 ms | 1.00 -`tuple_vector<MovableType>/erase ` | 104.6 ms | 106.3 ms | 0.98 -`tuple_vector<MovableType>/reallocate ` | 1.3 ms | 1.7 ms | 0.77 - - | | | -`tuple_vector<uint64>/erase ` | 3.4 ms | 3.5 ms | 0.98 -`tuple_vector<uint64>/insert ` | 3.4 ms | 3.4 ms | 0.99 -`tuple_vector<uint64>/iteration ` | 56.3 us | 81.4 us | 0.69 - -`tuple_vector<uint64>/operator[] ` | 67.4 us | 61.8 us | 1.09 -`tuple_vector<uint64>/push_back ` | 1.3 ms | 818.3 us | 1.53 + -`tuple_vector<uint64>/sort ` | 5.8 ms | 7.3 ms | 0.80 - | | | -`tuple_vector<uint64,Padding>/erase ` | 34.7 ms | 32.9 ms | 1.05 -`tuple_vector<uint64,Padding>/insert ` | 41.0 ms | 32.6 ms | 1.26 -`tuple_vector<uint64,Padding>/iteration ` | 247.1 us | 80.5 us | 3.07 + -`tuple_vector<uint64,Padding>/operator[]` | 695.7 us | 81.1 us | 8.58 + -`tuple_vector<uint64,Padding>/push_back ` | 10.0 ms | 6.0 ms | 1.67 + -`tuple_vector<uint64,Padding>/sort ` | 8.2 ms | 10.1 ms | 0.81 - | | | -`vector<AutoRefCount>/erase ` | 1.3 ms | 1.2 ms | 1.05 -`vector<MovableType>/erase ` | 104.4 ms | 109.4 ms | 0.95 -`vector<MovableType>/reallocate ` | 1.5 ms | 1.5 ms | 0.95 - | | | -`vector<uint64>/erase ` | 4.3 ms | 3.6 ms | 1.20 -`vector<uint64>/insert ` | 4.8 ms | 4.8 ms | 1.01 -`vector<uint64>/iteration ` | 71.5 us | 77.3 us | 0.92 -`vector<uint64>/operator[] ` | 90.7 us | 87.2 us | 1.04 -`vector<uint64>/push_back ` | 1.6 ms | 1.2 ms | 1.38 + -`vector<uint64>/sort ` | 7.7 ms | 8.2 ms | 0.93 - -First off, `tuple_vector<uint64>`'s performance versus `std::vector<uint64>` is -comparable, as expected, as the `tuple_vector`'s management for one type -becomes very similar to just a regular vector. The major notable exception is -the iteration case, which runs `eastl::find_if`. This -performance differences is a consequence of the iterator design, and how -it works with indices, not a direct pointer, so the code generation suffers slightly -in this compute-bound scenario. This is worth noting as a demonstration of a -case where falling back to pointer-based iteration by fetching the `begin` and -`end` pointers of that tuple element may be preferable, instead of using the -iterator constructs. - -The set of `tuple_vector<uint64,Padding>` tests are more interesting. -This is a comparison between a single `std::vector` with a -structure containing a `uint64` and 56 bytes of padding, and a `tuple_vector` with -two elements: one for `uint64` and one for 56 bytes of padding. The erase, -insert, push_back, and sort cases all perform at a similar relative rate as -they did in the `tuple_vector<uint64>` tests - demonstrating that operations -that have to touch all of elements do not have a significant change in -performance. - -However, iteration and operator[] are very different, because -those only access the `uint64` member of both `vector` and `tuple_vector` to run -some operation. The iteration test now runs 3x faster whereas before it ran -0.7x as fast, and operator[] runs 8.5x faster, instead of 1.1x. This -demonstrates some of the utility of `tuple_vector`, in that these algorithms end -up being limited by the CPU's compute capabilities, as opposed to being -limited by how fast they can load memory in from DRAM. - -In a series of other tests, generally speaking, `tuple_vector` tends to perform -on par with manual management of multiple arrays in many algorithms and -operations, often even generating the same code. It should be noted that -significant degrees of inlining and optimization are required to get the most out -of `tuple_vector`. Compared to accessing a series of arrays or vectors, -`tuple_vector` does perform a multitude of extra trivial function calls internally -in order to manage the various elements, or interact with `eastl::tuple` through -its interface, so running in debug configurations can run significantly slower -in some cases, e.g. sometimes running at 0.2x the speed compared to vector. - -## The problem of referencing tuple elements - -This will be experienced shortly after using `tuple_vector` in most capacities, -but it should be noted that the most significant drawback is that there is no -way to **symbolically** reference each tuple element of the `tuple_vector` - much -in the same way as `tuple`. For example, if translating a struct such as... - -``` -struct Entity -{ - float x, y, z; - float lifetime; -}; -``` -...to `tuple_vector`, it will exist as: - -``` -tuple_vector<float, float, float, float> entityVec; -``` - -...and can only be accessed in a manner like `entityVec.get<3>()` to refer to -the `lifetime` member. With existing tools, the only good alternatives are to -encapsulate each float as a separate struct to give it unique typenames... - -``` -struct entityX { float val; }; -struct entityY { float val; }; -struct entityZ { float val; }; -struct entityLifetime { float val; }; - -tuple_vector<entityX, entityY, entityZ, entityLifetime> entityVec; -``` -...and then access each tuple element by typename like -`entityVec.get<entityLifetime>()`; or, creating an enumerated value to replace -the indices... - -``` -enum EntityTypeEnum -{ - entityX = 0, - entityY = 1, - entityZ = 2, - entityLifetime = 3 -}; - -tuple_vector<float, float, float, float> entityVec; -``` - -...and then access each tuple element by the enumerated value: -`entityVec.get<entityLifetime>()`. - -Either way, there is a fairly significant maintenance and readability issue -around this. This is arguably more severe than with `tuple` on its own -because that is generally not intended for structures with long lifetime. - -Ideally, if the language could be mutated to accommodate such a thing, it would -be good to have some combination of typenames and symbolic names in the -declaration, e.g. something like - -``` -tuple_vector<float x, float y, float z, float lifetime> entityVec; -``` -and be able to reference the tuple elements not just by typename or index, but -through their corresponding symbol, like `entityVec.get<lifetime>()`. Or, it may -be interesting if the necessary `get` functions could be even automatically -generated through a reflection system, e.g. `entityVec.get_lifetime()`. -All of this remains a pipe dream for now. |