Skip to content

Commit

Permalink
Embed wiki sources in a 'docs' subdirectory (issue #157)
Browse files Browse the repository at this point in the history
The new subdirectory allows contributers to propose pull requests to
complete or fix the documentation, and also allows to enforce the good
practice of modifying the documentation along the code, making it easier
to track the changes to revert in the documentation if needed.

The new subdirectory was named "docs" to follow the Pitchfork project
layout conventions (https://github.com/vector-of-bool/pitchfork).

[ci skip]
  • Loading branch information
Morwenn committed Sep 23, 2020
1 parent bdb9844 commit cb2abe0
Show file tree
Hide file tree
Showing 22 changed files with 3,728 additions and 0 deletions.
84 changes: 84 additions & 0 deletions docs/Benchmarks.md
@@ -0,0 +1,84 @@
*Note: this page is hardly ever updated and the graphs might not reflect the most recent algorithms or optimizations. It can be used as a quick guide but if you really need a fast algorithm for a specific use case, you better run your own benchmarks.*

Benchmarking is hard and I might not be doing it right. Moreover, benchmarking sorting algorithms highlights that the time needed to sort a collection of elements depends on several things: the type to sort, the size of collection, the cost of comparing two values, the cost of moving two values, the distribution of the values in the collection to sort, the collection itself, etc... The aim of this page is to help you choose a sorting algorithm depending on your needs. You can find two main kinds of benchmarks: the ones that compare algorithms against shuffled collections of different sizes, and the ones that compare algorithms against different data patterns for a given collection size.

All of the graphs on this page have been generated with slightly modified versions of the scripts found in the project's benchmarks folder. There are just too many things to check; if you ever want a specific benchmark, don't hesitate to ask for it.

# Random-access iterables

Most sorting algorithms are designed to work with random-access iterators, so this section is deemed to be bigger than the other ones. Note that many of the algorithms work better with contiguous iterators; the resuts for `std::vector` and `std::deque` are probably different.

## Unstable sort for `std::vector` with 10⁶ `int`

![](http://i.imgur.com/RcREbKQ.png)

As we can see, even though its algorithmic complexity is O(n log n), `heap_sorter` is simply too slow on average, no matter the pattern. `std_sorter` actually performs worse than expected (the libstdc++ one), especially for some patterns. The best sorting algorithms for this configuration are `pdq_sorter`, `quick_sorter`, `spread_sorter` and `verge_sorter`.

`spread_sorter` is by far the best sorter when it comes to sorting truly random collections and is excellent when the number of values in the collection is low (16 values in the examples). When there are pipe-organ patterns, `verge_sorter` seems to be the best match since it takes advantage of big subsequences sorted in ascending or descending order. That said, both of these sorters use additional memory; `pqd_sorter` is the best sorter if a small memory footprint is needed.

## Unstable sort for `std::vector` with 10⁷ `int`

![](http://i.imgur.com/HNMO7zb.png)

Compared to the previous graph, a few things have changed: while it's still better than anything else for random distributions, `spread_sorter` has bad performance for the *Alternating* distribution. My guess is that it has to do with the fact that it's a radix-sort kind of algorithm and the fact that the *Alternating* case has both negative values and the widest range of values among the distributions doesn't help. `heap_sorter` is still the slowest thing ever.

`std_sorter` seems to reach some kind of worst case behaviour for some distributions, making it less usable than the other unstable sorters in this test case. Actually `quick_sorter` should also have some quadratic worst cases, but the median-of-9 pivot selection makes it harder to find such worst cases.

## Unstable sort for `std::deque` with 10⁶ `int`

![](http://i.imgur.com/0qU9cH7.png)

The results for `std::deque` are close to those for `std::vector`. The most notable difference is that `heap_sorter` is even slower than for `std::vector` compared to the other sorters; apparently, random-access has a noticeable cost when the iterators are not contiguous. Otherwise, the conclusions are pretty much the same than with `std::vector`: when the distribution is truly random, `spread_sorter` is still the obvious winner, while `verge_sorter` beats distributions where big chunks of data are already sorted. `pdq_sorter` is still the best match when a small memory footprint is required. `std_sorter` still has some kind of worst case behaviour for the *Pipe organ*, *Push front* and *Push middle* patterns.

## Stable sort for `std::vector` with 10⁷ `long double`

![](https://i.imgur.com/iQNaaAR.png)

## Heap sorts for `std::vector` with 10⁶ `int`

![](https://i.imgur.com/JxKhs5c.png)

As we have seen in the previous section, `heap_sorter` tends to be really slow. Its main use is as a fallback for introsort-like sorters that perform log n quicksort steps before switching to a heapsort if the collection is not sorted (it ensures the O(n log n) complexity of introsort-like algorithms). **cpp-sort** actually provides several heap sorters with different properties: all of them build a heap then remove elements one by one to sort the collection, but the underlying heaps are different (implementation-defined heap for `heap_sorter` since it uses `std::make_heap`, but likely a regular maxheap, forest of Leonardo heaps for `smooth_sorter`, and forest of perfect balanced binary heaps for `poplar_sorter`).

The regular `heap_sorter` has by far the best worst case and average performance. `smooth_sorter` and `poplar_sorter` are adaptative as can be seen when the collection to sort is already almost sorted, but their average running time is just too slow to be usable. It is worth noting that `smooth_sorter` was consistently slower than anything else with `-O2` (the graph above has been generated with `-O3`).

# Bidirectional iterables

Sorting algorithms that handle non-random-access iterators are generally second class citizens, but **cpp-sort** still has a few ones. Obviously quadratic algorithms such as insertion sort or selection sort are ignored for these benchmarks. The most interesting part is that we can see how generic sorting algorithms perform compared to algorithms such as [`std::list::sort`](http://en.cppreference.com/w/cpp/container/list/sort) which are aware of the data structure they are sorting.

## Sort `std::list` with 10⁶ `int`

![](http://i.imgur.com/pNfEsC1.png)

First of all, note that `default_sorter` uses [`self_sort_adapter`](https://github.com/Morwenn/cpp-sort/wiki/Sorter-adapters#self_sort_adapter), which means that in this benchmark, the sorting algorithm used by `default_sorter` is actually `std::list::sort`. Apparently, `std::list::sort` is efficient for many patterns, but it doesn't handle random distributions that much and also has some problems with *Alternating (16 values)*. On the other hand, `quick_sorter` works really well when there are few values, but doesn't really handle patterns as well as the other algorithms. `verge_sorter` is rather good when there are not many runs in the collection to sort, and falls back on `quick_sorter` when the runs aren't big enough, which means that it's good for some patterns, but always slower than `quick_sorter` otherwise (it's still more than decent when there aren't many values). Finally, `merge_sorter` adapts rather well to many patterns, especially when the collection is already sorted to some degree in ascending order; it is by far the best algorithm to handle the *Shuffled* distribution.

## Sort `std::list` with 10⁶ `long double`

![](http://i.imgur.com/FSoNXH2.png)

The results are pretty much the same than with integers. The most important difference is that `std::list::sort` isn't much slower since it does not move/swap values but relinks nodes instead, so no matter the size of the types to sort, relinking always has the same cost. Therefore, it's a bit better than with integers compared to the other sorting algorithms; I expect it to be the better and better when the types to sort are more expensive to move around.

# Forward iterables

Even fewer sorters can handle forward iterators; just like with bidirectional iterators, we only care about the sorters that are not almost always quadratic, which means that we care about the same sorters than before, except `verge_sorter` since it sometimes needs to call `std::reverse` which requires bidirectional iterators.

## Sort `std::forward_list` with 10⁶ `int`

![](http://i.imgur.com/HsiE2e7.png)

As expected, `default_sorter` actually calls `std::forward_list::sort`, which is a bit better than with bidirectional iterators compared to the other sorting algorithms, probably because it has fewer nodes to relink. `quick_sorter` is still excellent when there are not many different values in the collection to sort but is less efficient than `std::forward_list::sort` for almost every specific pattern. `merge_sorter` is pretty good when the collection to sort is mostly ascending since it adapts to ascending runs; it has the best worst case among the tested algorithms.

## Sort `std::foward_list` with 10⁶ `long double`

![](http://i.imgur.com/CKg7Ip5.png)

There is almost no difference compared to the previous graph except the overall time. Just like with bidirectional iterators, I would expect `std::forward_list::sort` to perform gradually better when the objects to sort are more expensive to move around. The good performances of `merge_sorter` in all the benchmarks are due to the fact that it can allocate memory to perform the merges; it adapts to the memory it is given, but it degrades to a O(n log² n) algorithm when no additional memory is available.

# Measures of presortedness

![](https://i.imgur.com/pe6YvuH.png)

This graph shows the running time of the different [measures of presortedness](https://github.com/Morwenn/cpp-sort/wiki/Measures-of-presortedness) when run on sequences of random integers of various sizes. While *Par(X)* seems to beat every other measure, be careful: it is highly adaptative, but its complexity is O(n² log n), which means that it can be as slow as *Dis(X)* or *Osc(X)* for some inputs, if not even slower.



32 changes: 32 additions & 0 deletions docs/Chainable-projections.md
@@ -0,0 +1,32 @@
*New in version 1.7.0*

Sometimes one might need to apply several transformations to the elements of a collection before comparing them. To support this use case, some projection functions in **cpp-sort** can be composed with `operator|`

```cpp
struct my_negate:
cppsort::utility::projection_base
{
int operator()(int value) const
{
return -value;
}
};
```

Making a function object inherit from `cppsort::utility::projection_base` allows it to benefit from the `operator|` overload used to compose projections; the projection inheriting from that class can appear on any side of the operator, and the other argument can be any suitable [*Callable*][callable]. Here is an example of what is possible with the custom projection defined above:

```cpp
// Create a vector of wrapper
struct wrapper { int value; };
std::vector<wrapper> vec = { /* ... */ };

my_negate projection;
// Applies &wrapper::value to the elements of vec, then my_negate
cppsort::poplar_sort(vec, &wrapper::value | projection);
```
The object returned by the utility function [`cppsort::utility::as_projection`][as_projection] also inherits from `cppsort::utility::projection_base`, making `as_projection` the proper function to turn any suitable projection into a projection composable with `operator|`.
[as_projection]: https://github.com/Morwenn/cpp-sort/wiki/Miscellaneous-utilities#as_comparison-and-as_projection
[callable]: https://en.cppreference.com/w/cpp/named_req/Callable
78 changes: 78 additions & 0 deletions docs/Changelog.md
@@ -0,0 +1,78 @@
This page describes the features that change in **cpp-sort** depending on the C++ version with which it is compiled (C++14 or later) as well as the support for miscellaneous compiler extensions; for a full changelog between actual releases, you can check the dedicated [releases page](https://github.com/Morwenn/cpp-sort/releases).

*The notes in this page are only valid for the latest versions of the 1.x and 2.x branches. If you are using an older version of the library, some of them might not apply.*

## C++14 features

While **cpp-sort** theoretically requires a fully C++14-compliant compiler, a few standard features are either not available or deactivated in popular compilers and the library tries to take those into account if possible.

**Performance improvements:**
* Sized deallocation: this C++14 feature is not always available (Clang requires `-fsized-deallocation` for example) and standard allocation functions typically don't take advantage of it. However, if `__cpp_sized_deallocation` is defined and the global deallocations functions are replaced with overloads that take advantage of sized deallocation, then several sorters will explicitly try to take advantage of it.

## C++17 features

When compiled with C++17, **cpp-sort** might gain a few additional features depending on the level of C++17 support provided by the compiler. The availability of most of the features depend on the presence of corresponding [feature-testing macros](https://wg21.link/SD6). The support for feature-testing macros being optional, it is possible that one of the features listed below isn't available even though the compiler is supposed to provide enough C++17 features to support it. If it is the case and it is a problem for you, don't hesitate to open an issue so that we can explicitly support the given compiler.

**New features:**
* `string_spread_sort` now accepts [`std::string_view`](https://en.cppreference.com/w/cpp/string/basic_string_view) and sometimes `std::wstring_view`.

This feature is made available through the check `__cplusplus > 201402L && __has_include(<string_view>)`.

* Sorter adapters have been updated to take advantage of deduction guides:

```cpp
// C++14
constexpr auto sort = schwartz_adapter<quick_sorter>{};
// C++17
constexpr auto sort = schwartz_adapter(quick_sort);
```

This notably makes measures of presortedness more usable with the few sorter adapters that make sense for them:

```cpp
// C++14
auto rem = indirect_adapter<decltype(probe::rem)>{};
// C++17
auto rem = indirect_adapter(probe::rem);
```

There is no specific check for this feature: the sorter adpater constructors have been written in such a way that implicit deduction guides work out-of-the-box.

* `indirect_adapter` and `out_of_place_adapter` return the result returned by the *adapter sorter*.

This feature is made available through the check `__cpp_lib_uncaught_exceptions`.

* New [`function_constant`](https://github.com/Morwenn/cpp-sort/wiki/Miscellaneous-utilities#miscellaneous-function-objects) utility to micro-optimize function pointers and class member pointers.

```cpp
insertion_sort(collection, function_constant<&foo::bar>{});
```

It sometimes results in fewer indirections than a raw `&foo::bar`, and can be subject to *empty base class optimization* when stored.

This feature is available when the feature-testing macro `__cpp_nontype_template_parameter_auto` is defined.

* The function pointer conversion operators of `sorter_facade` are now `constexpr` when possible.

This feature is made available through the check `__cpp_constexpr >= 201603`.

**Correctness improvements:**
* Some handy C++17 type traits such as `std::is_invocable` are manually reimplemented in C++14 mode while they are used as is in C++17 mode if available. It's likely that the C++17 implementation covers more corner cases and is thus more often correct than the manual C++14 implementation.

The C++17 traits are used as is when the feature-test macro `__cpp_lib_is_invocable` is defined.

## Other features

**cpp-sort** tries to take advantage of more than just standard features when possible, and also to provide extended support for some compiler-specific extensions. Below is a list of the impact that non-standard features might have on the library:

**Extension-specific support:**
* 128-bit integers support: `ska_sorter` has dedicated support for 128-bit integers (`unsigned __int128` or `__uint128_t` and its signed counterpart), no matter whether the standard library is also instrumented for those types. This support should be available as long as `__SIZEOF_INT128__` is defined by the compiler.

**Performance improvements:**
* Additional allocators: `merge_insertion_sorter` can be somewhat more performant when libstdc++'s [`bitmap_allocator`](https://gcc.gnu.org/onlinedocs/libstdc++/manual/bitmap_allocator.html) is available.

This improvement is made available through the check `__has_include(<ext/bitmap_allocator.h>)`, which means that it should be available for every compiler where `__has_include` and libstdc++ are available (old and new Clang, and more recent GCC).

*Changed in version 1.7.0:* `merge_insertion_sorter` uses a custom list implementation which does not need to take advantage of `bitmap_allocator` anymore.

* Bit manipulation intrinsics: there are a few places where bit tricks are used to perform a few operations faster. Some of those operations are made faster with bitwise manipulation intrinsics when those are available.
24 changes: 24 additions & 0 deletions docs/Comparators-and-projections.md
@@ -0,0 +1,24 @@
Most sorting algorithms in **cpp-sort** accept comparison and/or projection parameters. The library therefore considers these kinds of functions to be first-class citizens too and provides dedicated comparators, projections and tools to combine them and to solve common related problems.

All the functions and classes in **cpp-sort** that take comparison or projection functions as parameters expect [*Callable*][callable] parameters, which correspond to anything that can be used as the first parameter of [`std::invoke`](http://en.cppreference.com/w/cpp/utility/functional/invoke). This allows to pass entities such as pointers to members or pointer to member functions to the sorting algorithms; it should work out-of-the-box without any wrapping needed on the user side.

### Related utilities

Several of the [miscellaneous utilities][utilities] provided by the library are meant to interact with comparators and projections:
- [`as_comparison` and `as_projection`](https://github.com/Morwenn/cpp-sort/wiki/Miscellaneous-utilities#as_comparison-and-as_projection) are used to make it explicit whether an ambiguous function object should be used for comparison or for projection.
- [`as_function`](https://github.com/Morwenn/cpp-sort/wiki/Miscellaneous-utilities#as_function) can be used to turn any [*Callable*][callable] into an object invokable with regular parentheses.
- [`is_probably_branchless_comparison` and `is_probably_branchless_projection`](https://github.com/Morwenn/cpp-sort/wiki/Miscellaneous-utilities#branchless-traits) are type traits that can be used to mark whether functions are likely to be branchless when called with a specific type.
- [`identity`](https://github.com/Morwenn/cpp-sort/wiki/Miscellaneous-utilities#miscellaneous-function-objects) is the default projection returning the argument it is passed without modifying it.

### LWG3031

Sorters and adapters in the library accept comparators taking their parameters by non-`const` reference, and should work as expected as long as comparators do not actually modify their parameters in a way that affects the sort consistency. This is mostly meant to support legacy comparators, but it also covers more unusual use cases such as when a comparator needs to update the compared objects to store additional information (preferably fields that do not affect the result of the comparison).

This additional guarantee is allowed by the resolution of [LWG3031][lwg3031]. However when a comparator can take its parameters by both `const` and non-`const` reference, it is required to return consistent results no matter which overload is used (see the LWG issue for an example of inconsistent results).

*New in version 1.7.0*


[callable]: https://en.cppreference.com/w/cpp/named_req/Callable
[lwg3031]: https://wg21.link/LWG3031
[utilities]: https://github.com/Morwenn/cpp-sort/wiki/Miscellaneous-utilities

0 comments on commit cb2abe0

Please sign in to comment.