Home
PLF C++ Library - plf::list
Introduction
plf::list is a drop-in higher-performance replacement for std::list, with
(on average) *:
- 293% faster insertion
- 57% faster erasure
- 17% faster iteration
- 77% faster sorting
- 70% faster reversal
- 91% faster remove/remove_if
- 63% faster unique
- 811% faster clear (1147900% for
trivially-destructible types)
- 1248% faster destruction (6350% for
trivially-destructible types)
- 20-24% faster performance overall in ordered
use-case benchmarking(insertion, erasure and iteration on the fly and over
time)
Like std::list, plf::list maintains stable pointer/iterator links to list
elements regardless of erasure, insertion, merging, splicing, reversing and
sorting. It's functionality replicates std::list with two minor exceptions: the
removal of single-element and ranged splices when the source is not *this (splicing whole lists from source into *this is still available)
and inclusion of several useful functions: reserve(), shrink_to_fit(),
trim(), memory(), and overloads for single-element and ranged splice without a source list argument (source must always be *this). Non-member function overloads for std::erase, std::erase_if and std::swap are also implemented.
It achieves it's speed via the following:
- Unrolling the list ie. storing list nodes in memory
blocks rather than allocating each individually.
- Maintaining per-block free lists of erased nodes and
searching in an expansion pattern for non-empty free lists upon reinsertion
to a given location.
- My pointer-array sort hack.
The first increases insertion and erasure speed since individual per-element
allocations and deallocations are no longer necessary, and also increases the
speed of iteration due to increased element contiguousness in memory.
The second re-uses erased element memory locations, but in a way which promotes
element contiguousness (positioning the inserted element as close to the
insertion position as possible in memory). Several other methods were trialed,
but this had the best balance of reinsertion speed and subsequent iteration
speed.
The third is a simple method which allows any linked list to use
quicksort-style internal sorting rather than the slower mergesort-based
internal sorting typically utilized, but without invalidating
pointers/iterators to elements and without the disadvantage of slower sort
speed for larger-than-scalar types typically evidenced with a quicksort
method.
* These results are not achievable with std::list +
an allocator for the reasons described here. Results are
from benchmarks performed on a haswell-based CPU under GCC 8.1. Average
percentages are obtained by testing with numbers of elements ranging from 10 to
1000000, and for the use-case, insertion, erasure and iteration benchmarks,
from five different type sizes from char to a very large struct.
Motivation
Linked lists have many useful characteristics: they splice together without
losing pointer validity to contained elements, they sort quickly for large types,
and their erasure speed is very good. Unfortunately the base implementation of
most linked lists disallows decent performance for insertion - specifically,
most linked lists allocate per-node, which in the modern age is particularly
slow. The C++ std::list spec disallows clustered allocations by allowing
partial list splicing - ie. if a block of nodes is allocated instead of one,
the block cannot be split without invalidating pointers to nodes within the
block. This inability to allocate multiple nodes at once has a negative effect on insertion, iteration and erasure due to cache
effects and the increased number of allocation/deallocation calls.
plf::list is a re-think from the ground-up of how to structure a linked list
around modern hardware. This means grouped allocations, re-use of memory and
sort modification. The result is not something which can compete with vector
for iteration performance, but which easily beats it for non-back insertion and erasure
speeds, and increases it's competitiveness with aspects like sorting. It is
designed as a drop-in replacement for std::list, as well as a demonstration of
how lists can perform outside of the C++ specification's requirement of allowing partial-list
splicing.
plf::list is designed for better push_back() performance than push_front()
performance, in keeping with the most typical container usage patterns.
Implementation
Per-memory-block free list usage:
As detailed earlier, extensive testing showed block sizes of 2048 elements
to be optimal across types for balancing the reinsertion mechanism - what this
means is that, for any given block, the free list may contain element locations
in any area of that block, and, the location popped off the top of the list to
be used for reinsertion, may not be the location closest to the insertion
position (out of all locations in the free list). But if we decrease the block
size, we increase the probability of any free list location being closer to the
insertion position. However making the block size too low results in lowered
iteration speeds, as fewer elements are contiguous.
Other methods were tested for the reinsertion mechanism, with the dual aim
of quick reinsertion and finding a reinsertion location as close to the
insertion position as possible. In no particular order they are:
- Reversed jump-counting skipfield notation. It is possible to use a
high-complexity jump-counting skipfield to find the nearest non-skipped element from any
given skipped element. The details of this are too complex to be discussed
here but may be found in the full
(but innaccurate and out-of-date) version of the jump-counting skipfield
paper under "As a measure of comparitive 1-dimensional distance". In
this context we used the skipfield in reverse, notating erased element
locations as unskipped and non-erased element locations as skipped. From
this we could find the nearest erased element location to the insertion
position (non-erased element) very quickly. But unfortunately, the time
taken to update the jump-counting skipfield upon insertion and erasure was
too long, and this was the slowest method.
- Global free list. This method has been used before in some other
linked list implementations (including a javascript one). You use a global
free list of all erased element locations across all memory blocks within
the linked list, and simply pop a location from the top of the list when
re-inserting. This method has several failings - the first is that upon
removal of a memory block, one must process the entire free list to remove
all erased locations from that block. For large blocks, this can be pretty
slow. Secondly, there is no way of ensuring that the selected location is
close to the insertion position, other than processing the entire free
list, which again is slow. This means that inevitably you end up with large
memory distances between two linked nodes in the linked list, which is bad
for iterative performance.
- Per-memory-block free lists. This works by having separate free
lists for each memory block. In the event that the free list for the memory
block that the insertion position is in, is empty, the mechanism radiates
outward, checking the free lists for the right and left memory blocks in
sequence until a non-empty free list is found. While this method does not
necessarily guarantee that the chosen location will be as close as possible
to the insertion position, it is the fastest overall, and, given an
appropriate block size, the effect of the memory distance from the
insertion position is mitigated to an extent.
As mentioned, the latter is the chosen method - however there were several
sub-methods of this which did not get selected. The first was to process the
selected free list to find the location within that free list closest to the
insertion position. This did not result in a net gain when benchmarked with
subsequent iterations balanced against the additional reinsertion cost. The
second sub-method was searching from the last memory block in the list to the
first sequentially instead of radiating outwards from the insertion position's
block - again this was slower, due to the likelyhood of finding a location
further away in memory than with the radiant pattern.
The reason why the radiant pattern works best is because in terms of
iteration, if you find a location in as close a memory block as possible, you
increase the overall statistical likelihood that the linked list sequence will
also follow the sequence of the memory blocks in memory (provided the blocks
are allocated close to each other, this will affect cache locality).
The pointer-array sort hack:
Normally it is impossible for a container with a bidirectional iterator
(such as a list) to utilize std::sort, as it requires a random-access iterator.
This method allows any container to use std::sort (or any sort technique that
requires random-access iterators). It has a memory cost of N pointers over a
normal list sort - one pointer for each element in the container. Each element
has it's node's memory address inserted into an array - the order of the
insertion is not important. Hence, while with a traditional linked list the
list must be iterated over to add the addresses to the array, with plf::list
the nodes of all memory blocks can be processed sequentially in order, speeding
up the process of filling the array.
Once this array of N pointers is filled with the addresses of all active
nodes in the list, we then use a templated dereferencing functor in order to
sort these pointers based on the value of the elements within the nodes that
they point to:
template <class comparison_function>
struct sort_dereferencer
{
comparison_function stored_instance;
sort_dereferencer(const comparison_function &function_instance):
stored_instance(function_instance)
{}
sort_dereferencer()
{}
bool operator() (const node_pointer_type first, const node_pointer_type second)
const
{
return stored_instance(first->element, second->element);
}
};
This function object takes whatever sort function is supplied to the sort
operation, and uses it to sort the pointers instead of the elements. Since an
array is a linear structure and can hence utilize a random-access iterator, we
can now sort the array of pointers using std::sort (or any
technique), the function object above and whichever sort function is
supplied.
Once the pointers are sorted according to the sort function and the elements
themselves, the final process is simple;
- Process array sequentially, for every pointer at index i, dereference to
node and set previous and next pointers to the addresses of i - 1 and i + 1
respectively.
- Set the node pointed to by array index 0 as the front node, and the node
pointed to by the last pointer in the array as the back node.
The resultant sort process is roughly 72% faster than std::list's sort, when
averaged across all datatypes and numbers of elements. It is not, unlike
mergesort, stable (in this context, stable means two contiguous objects with
the same sort value will be in the same order once sorted), because std::sort
is not guaranteed to be stable, but std::stable_sort can be used instead if
this is important.
Inner structure:
The internal structure of plf::list is as follows: a custom vector holds the
individual memory blocks that the elements are stored within, plus each memory
block's metadata (this combined with the memory block is termed a 'group').
Part of the metadata is each block's 'free list' of erased memory locations.
When a memory block becomes empty of elements it is either freed to the OS or
moved to the back of the vector for subsequent re-use in future. The
implementation defines the protocol for deciding whether or not to free the
block, but in my implementation the decision is based on whether or not the
memory block is of maximum size, and also whether it is close to the back of
the vector.
When an insertion occurs, if there are no erased locations the new element
is pushed to the next available element location in the last memory block and
then linked into insertion position. Otherwise the free list of the memory
block which the insertion position is within is checked - if empty, we check
each memory block to the left and right in sequence until a non-empty free list
is found. The selected non-empty free list has it's top element popped off and
re-used.
To avoid double-destruction of elements when clearing or destroying a
plf::list, element nodes which are added to their memory block's free list have
their 'next' pointer set to NULL - since free lists only utilize the previous
pointer, and non-freelist node pointers cannot be == NULL, this has no effect
on operation. When a plf::list is cleared or destroyed, instead of iterating
over the list normally, we can more quickly iterate directly over the memory
blocks themselves, destroying any element of any node where the next pointer is
!= NULL. The reason this is faster is because in this method, each subsequent
node is contiguous in memory while iterating, compared to a regular list
traversal which can jump all over the place.
License
plf::list is under a permissive zlib license. This means:
Software is used on 'as-is' basis. It can be modified and used in commercial
software. Authors are not liable for any damages arising from its use. The
distribution of a modified version of the software is subject to the following
restrictions:
- The authorship of the original software must not be misrepresented,
- Altered source versions must not be misrepresented as being the original
software, and
- The license notice must not be removed from source distributions.
Download
Download here or view
the repository
The list library is a simple .h header file, to be included with a #include
command.
In addition if you are interested in benchmarking you can also download the plf benchmark suite.
Function Descriptions and syntax
plf::list meets the technical specifications and has the same functions and
iterator functions as std::list, with the
exception of the removal of range or single-element splices between plf::list's. Only full list (ie. all element) splices between plf::list's are
allowed. Range and single-element splices within a plf::list (ie. source == *this) are allowed and are detailed below. In addition, plf::list does not guarantee that contiguous and equal
elements will be in the same order after sorting as std::list does (ie. the
sort style is not a 'stable'-type). This is due to the way that plf::list::sort() gathers element pointers in the 'gather' phase of the algorithm (see above).
To specify an alternative sort technique, #define PLF_SORT_FUNCTION as the name of the sort function you wish to use.
The sort function in question must take the same parameters as std::sort. An example would be #define PLF_SORT_FUNCTION std::stable_sort
. This macro must be defined before plf_list.h is #include'd.
Additional functions specific to plf::list follow:
void trim_capacity() noexcept
Removes any trailing groups within the container substructure, which may
be present in the scenario of many erasures. Unlike a regular
shrink_to_fit, does not shrink capacity to the size needed to contain the
currently-stored elements, simply frees up unused groups. Runtime cost is
significantly lower than shrink_to_fit, but may not free as much memory.
Example: i_list.trim_capacity();
void shrink_to_fit()
Reduces container capacity to the amount necessary to store all
currently stored elements, consolidates elements and removes any erased
locations. Invalidates all pointers, iterators and references to elements
within the container.
Example: i_list.shrink_to_fit();
void reserve(size_type reserve_amount)
Preallocates memory space sufficient to store the number of elements
indicated by reserve_amount
. If possible the algorithm will
re-use existing empty memory blocks, so long as they are large enough. This
function is necessary due to the growth factor of plf::list and the affect
of many needless smaller memory block allocations on performance.
Example: i_list.reserve(15);
size_type capacity() const noexcept
Returns total number of elements currently able to be stored in
container without expansion.
Example: std::cout << i_list.capacity()
<< std::endl;
void splice(const_iterator position, const_iterator location) noexcept
Equivalent to std::list's single-element splice operation if the source is *this. Reorders the list so that the element at 'location' in the list is
before 'position'. O(1) time complexity operation with no copying or moving
(only previous and next pointers are altered. Good for shifting locations
of large elements, non-trivially-copyable elements, and
non-copyable/movable elements. If position and location are the same, no
operation will occur. If location source is not *this behaviour is undefined.
Example: plf::list<int> list1 = {1, 2, 3,
4, 5};
plf::list<int>::iterator it = --(list1.end());
// Shift 5 in front of 1:
list1.splice(list1.begin(), it);
void splice(const_iterator position, const_iterator first, const_iterator
last) noexcept
Equivalent to std::list's range splice operation if the source of the range is *this. Same as single splice but with a range of first->(last - 1)
ie. last points to the element one-past the back element of the range, not the back element itself. Also O(1). If first is later
than last in the list's iterative sequence, behaviour is undefined. If
position is within the range of first->last, behaviour is
undefined. If range source is not *this, behaviour is undefined.
Example: plf::list<int> list1 = {1, 2, 3,
4, 5};
plf::list<int>::iterator it1 = list1.begin();
plf::list<int>::iterator it2 = std::advance(list1.begin(), 2);
plf::list<int>::iterator it3 = list1.end();
// Shift 1, 2 to after 5:
list1.splice(it3, it1, it2);
template <class predicate_function>
iterator unordered_find_single(predicate_function predicate) const noexcept
iterator unordered_find_single(value_type element_to_match) const noexcept
Searches across the entire list in an unordered fashion (iterating
linearly in memory over the node blocks rather than following the linked
list sequence - much faster than std::find()) and returns an iterator to
the first node which matches the supplied element. Will return end() if the
element isn't found.
Example: plf::list<int> list1 = {1, 2, 3,
4, 5};
plf::list<int>::iterator it1 = list1.unordered_find_single(3);
std::cout *it1;
template <class predicate_function>
plf::list<plf::list<value_type>::iterator>
unordered_find_multiple(predicate_function predicate, size_type
number_of_elements_to_find) const noexcept
plf::list<plf::list<value_type>::iterator>
unordered_find_multiple(value_type element_to_match, size_type
number_of_elements_to_find) const noexcept
Searches across the entire list in an unordered fashion for the supplied
element, collecting as many iterators to matching elements as the user
specifies, returns a plf::list of iterators pointing to all nodes which
match the element when either (a) the number of elements is reached or (b)
the end of the list is reached. Much faster than std::find().
Example: plf::list<int> list1 = {1, 2, 3,
4, 5, 5, 4, 3, 2, 1, 3, 5, 6, 1, 3};
plf::list<plf::list<int>::iterator> nodes_found =
list1.unordered_find_multiple(1, 2);
std::cout *(nodes_found.begin());
template <class predicate_function>
plf::list<plf::list<value_type>::iterator>
unordered_find_all(predicate_function predicate) const noexcept
plf::list<plf::list<value_type>::iterator>
unordered_find_all(value_type element_to_match) const noexpect
Searches across the entire list in an unordered fashion for the supplied
element and returns a plf::list of iterators pointing to all nodes which
match the element. Much faster than std::find().
Example: plf::list<int> list1 = {1, 2, 3,
4, 5, 5, 4, 3, 2, 1};
plf::list<plf::list<int>::iterator> nodes_found =
list1.unordered_find_all(3);
std::cout *(nodes_found.begin());
Benchmarks
Benchmark results for plf::list v1.85 under GCC 9.2 x64 on an Intel Xeon
E3-1241 (Haswell) are available here.
Some results for an early version of plf::list under GCC 5.1 x64 on an Intel
E8500 (Core2) are here.
Frequently-asked Questions
Can I achieve the same performance by using an allocator with
std::list?
There are two parts to the answer to that question, but both say
'no'.
Part 1 involves the standard operations: insert, erase, iteration. In
order for a std::list + allocator to maintain the 16% iteration performance
increase that plf::list has (which is the result of more contiguous
storage), after multiple non-back/front erasures and insertions, the actual
memory location of an inserted element must be as close as possible to the
element pointed to by 'it' in 'list.insert(it, value);'. This means the
allocator has to be able to take memory hints for allocating new elements,
but also the linked list itself must supply these hints. Given that no
current std::list implementations (that I am aware of) do this, this will
not occur. However even if it did occur, most allocators use global
free-lists or other less-efficient techniques for determining the closest
insertion point for a newly-allocated element, so you would not gain the
same post-erase insert performance unless you used the same (or better)
techniques as plf::list for managing memory and figuring out optimal
insertion locations.
Part 2 involves the less-used operations: reverse, clear, find_if, find,
sort, and list destruction. These all utilize plf::list's contiguous node
chunks to linearly iterate across nodes regardless of order for part or all
of their function - as opposed to following the linked list's iterative
sequence. What this means is the cache-friendliness and
prefetch-friendliness of these operations is greater than a std::list +
allocator can be, because a std::list cannot iterate linearly across it's
'owned' memory space in the allocator. In addition, even if you somehow
managed to get a std::list implementation talking to an allocator well
enough that all of this occurred, you would lose cycles due to the
interaction. eg. having to use an iterator supplied by the allocator, along
with whatever overhead was incurred by the allocator ensuring that unused
memory spaces are skipped (jump-counting skipfield, for example).
Are there any advantages to using std::list + an allocator instead of
plf::list?
Only one, and that is that partial (range) splicing between std::lists
using the same allocator can occur without creating problems. There are no
performance advantages, that I am aware of.
What are the thread-safe guarantees?
Here is a (non-concurrent implementation of) std::vector's thread-safe
matrix to see which basic operations can occur at the same time:
std::vector |
Insertion |
Erasure |
Iteration |
Read |
Insertion |
No |
No |
No |
No |
Erasure |
No |
No |
No |
No |
Iteration |
No |
No |
Yes |
Yes |
Read |
No |
No |
Yes |
Yes |
Here is plf::list's matrix:
plf::list |
Insertion |
Erasure |
Iteration |
Read |
Insertion |
No |
No |
Mostly** |
Yes |
Erasure |
No |
No |
Mostly*** |
Mostly* |
Iteration |
Mostly** |
Mostly*** |
Yes |
Yes |
Read |
Yes |
Mostly* |
Yes |
Yes |
* Erasures will not invalidate iterators
unless the iterator points to the erased element.
** Iteration will not be affected by insertion unless the element being
iterated to/from is the element which is being inserted in front of.
*** Iteration will not be affected by erasure unless the element being
iterated to/from is the element being erased.
Version history
- 2024-11-04: v2.7.10 - Corrections to const use on move assignment/construction in iterators. Corrections to reverse_iterator operators.
- 2024-10-14: v2.7.9 - West const 4 lyfe - there was inconsistent use of const throughout due to my misunderstanding of how typedefs change const expession in C++.
- 2024-09-06: v2.7.8 - Minor optimisations, update to test suite.
- 2024-07-17: v2.7.7 - Removal of needless assert in operator ==. Simplified some asserts and removed some repeated comments.
- 2024-07-17: v2.7.6 - Fix to support under GCC5 with -std=c++11, courtesy of Martin Valgur.
- 2024-04-21: v2.7.5 - Minor code reduction.
- 2024-04-10: v2.7.4 - Change to vcpkg-compatible versioning. plf::make_move_iterator changed to be static. plf::equal_to uses references now and is entirely noexcept.
- 2024-03-24: v2.73 - == and != operators changed to be friend functions.
- 2024-02-07: v2.72 - Corrections to C++17 and below for list and C++03 and below for test suite.
- 2024-01-03: v2.71 - Macro code reduction.
- 2023-11-25: v2.70 - Swapped some manual swapping with std::swap where appropriate.
- 2023-10-24: v2.69 - Implemented predicate-based versions of unordered_find_single, unordered_find_multiple and unordered_find_all. Original functions still exist. Fixed clang issue/removed redundant code. stable_sort changed back to sort - the reason for this is that plf::list iterates linearly over the element memory blocks to get pointers to elements instead of following the list sequence, as the former is a lot faster - but because of this it ends up gathering the pointers out-of-sequence, so using stable_sort would make no difference.
- 2023-10-10: v2.66 - Internal sort algorithm changed to std::stable_sort instead of std::sort to be more compatible with std::list. No performance difference.
- 2023-10-06: v2.65 - Min/max block capacity limits changed to static constexpr functions.
- 2023-08-10: v2.64 - Update to address shared tools.
- 2023-07-08: v2.63 - Improvement for support on older compilers.
- 2023-07-05: v2.62 - Bypass buggy MSVC /W4 warning, comment corrections.
- 2023-06-23: v2.61 - Improvement for support on older compilers.
- 2023-06-06: v2.60 - Technical correction to pointer casting to void *.
- 2023-02-15: v2.59 - Correction of internal namespacing for iterator subtype to avoid external namespace mis-matches.
- 2023-02-03: v2.58 - More accurate compiler/library detection for C++20 support, C++20 support disabled for apple clang as it is not as up to date as non-apple clang.
- 2023-01-24: v2.57 - Fix to reverse_iterator(iterator) constructors and '= iterator' operators.
- 2023-01-22: v2.56 - Fix to swap when using allocators which provide copyable but not movable pointers.
- 2023-01-21: v2.55 - Corrected const-correctness of reverse_iterator operators and constructors. Major fix to splice and merge - previously was not transferring erased element free list from source to destination. Added const_reverse_iterator versions of rbegin() and rend(). Minor code reduction.
- 2023-01-11: v2.53 - Support for disabling exceptions under GCC, Clang and MSVC. Will (a) now compile under gcc/clang when -fno-exceptions is specified and (b) will terminate in cases where exceptions would normally be thrown.
- 2022-10-02: v2.52 - Correction to move constructor. Correction for older compilers which partially support C++11 like MSVC2010. Removed plf::rand dependency from test suite.
- 2022-09-03: v2.51 - Minor optimisation to algorithm to find erased node closest to insertion position. Correction for use with stateful allocators. More items moved into shared plf tools. Corrections for when allocator supplies non-trivial pointers.
- 2022-08-27: v2.50 - Sentinel overloads removed, ranges v3 support added for insert/constructors. Performance improvements. Correct support for stateful allocators. trim() renamed to trim_capacity() ('trim' has a specific meaning in some STL scenarios). C++20 correctness fixes. Constructor corrections. Some tooling functions made public for access across multiple plf:: libraries. Fix to splice when moving the last element in the list.
- 2021-12-11: v2.07 - Added spaceship operator for three-way comparison of lists.
- 2021-11-25: v2.06 - Fix regression bug to shrink_to_fit() and restore missing test suite checks.
- 2021-10-31: v2.05 - Range constructors/inserts/assigns updated so that the slower form that is required to match sentinels, will not match iterator types which can be converted to one another eg. const_iterator and iterator. This means that, for example, a call to range-assign using colony's const_iterator and iterator will be able to use colony's faster ADL distance() overload rather than just doing a straight for-loop to determine length of range (necessary with sentinels, as there is no overload for distance() for sentinels).
- 2021-09-18: v2.04 - Support for range construction with differing iterator types. Correction to range-insert/range-assign with differing iterator types. Correction to compiler feature detections.
- 2021-06-03: v2.03 - Correction to splice() when beginning of range == begin().
- 2021-06-02: v2.02 - Optimisation and code reduction for fill/initializer_list/range insert/assign/construction/operator =. Also optimisations to shrink_to_fit(), removal of needless overload for range-move assign() (will be forwarded onto move-range-insert regardless).
- 2021-06-01: v2.01 - Correction to fill/initializer_list/range insert/assign when previous erasures have occured.
- 2021-05-26: v2.00 - Optimisation to range/initializer_list insert/assign. Correction to the C++20-specific range insert which allows for two different iterator types.
Internal sort algorithm used by sort() now specifiable by defining the PLF_SORT_FUNCTION macro as the name of your function (std::sort is used by default when this macro is not defined). The sort function in question must be able to take the same arguments, in the same order, as std::sort. An example would be std::stable_sort. Previously this function would use gfx::timsort if that function's macro were defined, but no others.
Added move_iterator support for range insert/assign.
Added sentinel support for assign.
- 2021-05-25: v1.90 - Correction to compiler detection routines for clang/gcc running in MSVC environment.
- 2021-05-20: v1.89 - Correction to compiler detection routines for clang - only libc++ 13 and above support the concepts library.
- 2021-05-09: v1.88 - Add throw length_error to reserve.
- 2021-03-29: v1.87 - Correction to rbegin() and rend() const status. Added support for sentinels/differing iterator types to range insert.
- 2021-03-22: v1.86 - Minor optimizations, correction to MSVC compiler support for CPP20.
- 2021-01-13: v1.85 - Reduction of compiler feature macros for reasons of readability, code size and convenience of updating between the different containers. Hopefully, PLF_ by itself will be enough to disambiguate these from similarly-named macros. If not, will revert back. Improvement of a few compiler rules.
- 2021-01-11: v1.84 - Added another static_cast to work around GCC's -Werror=conversion warning. Corrections to compiler feature detection.
- 2021-01-10: v1.83 - Added static_cast's to work around GCC's -Werror=conversion warnings. Correction to range reorder(), now correctly uses 'last' argument as the one-past-the-back iterator as per std:: guidelines, rather than back iterator of the sequence the user wishes to move. Changed iterators to const_iterators in reorder. Renamed reorder to splice overloads as didn't realise std::list's splice allows for splicing within *this.
- 2021-01-09: v1.82 - Correction to group allocations under C++98/03. Removed needless static_cast.
- 2021-01-03: v1.81 - plf::pcg_rand renamed to plf::rand and modified to enable better support for C++03 and lower compilers. Fix accidental regression for memory() function name.
- 2020-12-26: v1.80 - Corrections to compiler feature detection code. Test suite now uses plf::pcg_rand instead of xor_rand.
- 2020-11-28: v1.79 - default value fill constructor added. Minor optimization to unordered_find_multiple().
- 2020-11-06: v1.78 - free_unused_memory() renamed to trim(). Macro renaming. Removal of some unnecessary assertions. Addition of std::erase() and std::erase_if() non-member function overloads for colony container.
- 2020-10-25: v1.77 - memory() renamed to memory().
- 2020-09-19: v1.76 - Correction to non-member swap, should've been in namespace std.
- 2020-07-07: v1.75 - Range-insert/constructor optimization now available
in C++11-and-above modes.
- 2020-06-27: v1.74 - Reduction of compiler switches.
- 2020-05-15: v1.73 - Added const function specifier to unordered_find
functions.
- 2020-05-03: v1.72 - Moved some internal insert/emplace functionality to
sub-functions. Changed some internal iterator use to const_iterator,
partially to match std::list. Minor optimisation to operator ==.
- 2020-04-10: v1.71 - Add [[nodiscard]] for empty(). Correction to test
suite.
- 2020-03-22: v1.70 - Added list operator = for std::initializer_list.
Updated noexcept conditions for swap and move operations. Added code to
automatically reserve necessary capacity in range/initializer_list
insert/construct/operator =, if the range in question uses random_access
iterators. Bugfix for unordered_find_single in C++03/C++11.
- 2019-12-02: v1.69 - Updated range-erase to be in line with std::list
requirements ie. now returns end iterator. Also slightly more streamlined
aesthetically, code-wise. Thanks to Gene Harvey (github user gharveymn) for these changes.
Corrections to comments. Additional reverse() test.
- 2019-11-27: v1.68 - Added asserts to move constructors, removed
unnecessary asserts from assignments between const and non-const iterators.
Added ability to copy-construct between reverse_iterator and
const_reverse_iterator.
- 2019-11-18: v1.67 - Improvements to performance in erase, reorder based
on advice from Elliot Goodrich - Thanks!
- 2019-10-31: v1.66 - Changed unordered_find_multiple() to
unordered_find_all(), unordered_find_multiple() now collects a user-defined
number of elements.
- 2019-10-30: v1.65 - Added unordered_find_single() and
unordered_find_multiple() functions.
- 2019-07-22: v1.64 - Corrections to: comments, destruction, clear(),
shrink_to_fit(), reserve(). Mainly for situations with one of these
functions followed by the other, or by insert()/push_back()/emplace().
Added test cases to test suite.
- 2019-04-15: v1.60 - Constexpr added to type traits if blocks and some
other scenarios, for compilers which support it - including MSVC2017 with
std:c++17 switch. This also creates performance benefits in some scenarios
both in debug and release builds.
- 2019-04-12: v1.59 - shrink_to_fit() is now a valid operation for element
types which are move-constructible but not copy-constructible, when type
traits are supported by compiler. Will also be faster for
non-trivially-copyable-but-trivially-movable types.
- 2019-03-19: v1.58 - Removed unnecessary const casts (to avoid GCC
warning).
- 2019-02-28: v1.57 - Added static_cast's to work around GCC's
-Werror=sign-conversion warnings.
- 2019-01-29: v1.56 - Move constructors corrected so that resultant
moved-from container is reusable.
- 2019-01-23: v1.55 - Minor optimization to unique(), remove() and
remove_if().
- 2019-01-16: v1.54 - Correction to library detection features to include
type traits for libc++. Removed support for plf::timsort, added support for
gfx::timsort (differences between plf and gfx version were not substantive
enough to justify maintaining a fork of the original code).
Optimization/simplification to reverse().
- 2018-11-16: v1.52 - Brought remove(), remove_if(), and unique() in line
with C++20 std::list guidelines (each now returns number of removed
elements). Also fixed bug where remove/remove_if would throw an exception
if list was empty. Correction to max_size() for use with allocator_traits
and to be in line with C++20.
- 2018-10-28: v1.51 - Minor optimizations to move assignment and blank().
Blank() optimization improves move constructor, move assignment,
destructor, splice. Move assignment optimization also improves regular
assignment, shrink_to_fit, splice, merge & swap.
- 2018-08-01: v1.50 - Performance increases, corrections and improvements
to single insert, fill-insert and reserve. End result is between 2-7%
overall performance increase for large (> 10000, generally) numbers of
elements in general use tests. Reversion from printf in test suite to
iostream, as printf created problems for dealing with size_type across
platforms.
- 2018-07-11: v1.42 - Removal of c++11 code from group_vector swap - will
never be reached as group_vector swap is only used during swap() under
C++03/98.
- 2018-07-11: v1.41 - Fix for regression in 1.40 - fix to swap.
- 2018-07-07: v1.40 - Optimization to swap. Comment correction/updates.
- 2018-07-06: v1.39 - Corrected nothrow_copy_constructible check under
move-insert to nothrow_move_constructible check. Comment addition/updates.
Additional emplace test in test suite. Added void * casts to deal with GCC
8.1's garbage warning messages around memset/memmove/memcpy.
- 2018-06-27: v1.38 - Added support for comparing and assigning const and
non-const iterators/reverse_iterators eg. if(const_it == it). (c++14
compatibility)
- 2018-06-21: v1.37 - Change iterator operators ++, -- & = to return
iterator references rather than iterators (standards compliance). Change of
variable name in private destroy_all_node_pointers function to avoid MSVC
warnings. Change of sort dereferencer function to allow for non-const sort
functions (with internal state for example). Many thanks to github user
dinghram for informing me of these.
- 2018-06-18: v1.35 - Fix mistaken return value on emplace_back/front. Also
avoid possible call to iterator dereferencing function on return value of
emplace_back, emplace_front. Added emplace_back/front value return tests to
test suite. Cleanup of test suite.
- 2018-06-17: v1.34 - Adjusted emplace_back and emplace_front to comply
with C++17 standard (both return a reference to the emplaced element now).
Thanks to github's Eremiell for mentioning this to me.
- 2018-05-12: v1.33 - Fix for any compiler with ridiculous memcpy
implementation. Avoid out-of-bounds access for edge case in C++03/98. Minor
fix for C++03/98.
- 2018-05-10: v1.31 - Major crash fix for linux gcc/clang and possibly
other systems (not visible in windows msvc/mingw-gcc). Optimisation for
some cases where either pointers are non-trivial or framework is
c++03/98.
- 2018-05-09: v1.22 - Corrected missing semicolon in compiler feature
definitions.
- 2018-04-22: v1.21 - Code comment cleanup. It is now valid behaviour to
call range-erase with two equal iterators ie. an empty range.
- 2018-04-13: v1.20 - Mistake in NOEXCEPT_MOVE_ASSIGNMENT compiler feature
macro, fixed.
- 2018-04-05: v1.19 - Const variant of being/end changed to const_iterator
to conform with the C++ standard list.
- 2018-04-02: v1.18 - Minor code cleanup/renaming. Corrections to compiler
feature support detection. Corrections to some noexcept function statuses.
Reversion of insert from a function call to emplace, back to a separate
function - as this was causing copy-construction on non-trivial types,
leading to additional temporaries and severe performance drop on large
non-trivial types. Minor optimisation for C++03/98. Correction to
constructors to fix segfault on move-construct/move-assignment when source
list is empty.
- 2018-03-12: v1.16 - Optimisation to reverse() for under 200 elements.
- 2018-03-09: v1.15 - Corrections to several functions under C++03/98
including splice, merge, swap, operator =, shrink_to_fit, copy
construction. Minor optimization to operator =. Corrections to test_suite
under C++03/98. Fix for compilers where move semantics are supported but
not variadics eg. MSVC2010.
- 2018-02-07: v1.12 - Removed 1.10 code, replaced freelist node detection
condition with "next == NULL" (previously next == previous). I forgot that
this was possible, as it wasn't with earlier pre-v1 betas, due to list's
architecture. Removes the need for costly workarounds due to the detected
edge-cases as described by v1.10. Also fixed a minor edge-case bug
introduced in v1.10.
- 2018-01-05: v1.10 - Edge-case corrections for destructor, clear(),
assign(), remove() and remove_if(). Thanks jdmairs for the bug report.
- 2017-11-15: v1.08 - Minor optimize to erase, removal of references for
some iterator operations and functions.
- 2017-11-1: v1.07 - Correction to no-exception compile-time checking for
insert/etc. Minor optimization/correction to unique(). Inline-hinting
change to insert.
- 2017-10-18: v1.06 - reorder() (single and range) functions added. Minor
code optimization.
- 2017-10-11: v1.05 - Codegen reduction for insert, move-insert for
compilers with variadic parameter support. Bug-fix to emplace.
- 2017-10-05: v1.04 - Performance improvement for sort, destruction (with
non-trivially-destructible types), remove, remove_if and reverse, when
numbers of erasures are low. Optimization for remove.
- 2017-09-16: v1.02 - Avoid generating exception try-catch code for
nothrow-constructible types.
- 2017-09-11: v1.01 - Minor optimization to fill-insert. Removal of some
pedantic warnings.
- 2017-08-21: v1.00 - First release
Contact:
plf:: library and this page Copyright (c) 2024, Matthew Bentley