Skip to content

Commit

Permalink
Add an explicit stable_adapter<verge_sorter> specializations
Browse files Browse the repository at this point in the history
An likewise for stable_adapter<verge_adapter>. Instead of simply relying
on make_stable, the new specializations use a variant of vergesort that
detects strictly descending runs instead of non-ascending ones, and
wraps the fallback sorter in stable_t.

The technique of detecting non-descending and strictly descending runs
is a trick borrowed from timsort in order to preserve stability in
natural mergesort algorithms: descending runs are reversed in-place, so
equivalent elements wouldn't retain their original order when reversed
in such runs.

While verge_adapter does not handle bidirectional iterators, the
underlying code was changed so that it will be easier to do in the
future, but currently this involves some dirty tricks with a stripped
down implementation of a sized iterator. This will make the transition
to C++20 ranges and standard sized iterators and sentinels easier in the
long term. Meanwhile it is just an implementation detail.

This commit addresses external issue Morwenn/vergesort#11 and somehow
Morwenn/vergesort#7 too. cpp-sort is a better recipient for those
variations on vergesort than the standalone project, but some
cross-project documentation will be needed anyway.
  • Loading branch information
Morwenn committed Dec 21, 2020
1 parent 6f9738d commit affe28b
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 87 deletions.
8 changes: 8 additions & 0 deletions docs/Sorter-adapters.md
Expand Up @@ -244,8 +244,10 @@ One can provide a dedicated stable algorithm by explicitly specializing `stable_

* [`default_sorter`][default-sorter]
* [`std_sorter`][std-sorter]
* [`verge_sorter`][verge-sorter]
* [`hybrid_adapter`][hybrid-adapter]
* [`self_sort_adapter`][self-sort-adapter]
* [`verge_adapter`][verge-adapter]

If such a user specialization is provided, it shall alias `is_always_stable` to `std::true_type` and provide a `type` member type which follows the rules mentioned earlier.

Expand Down Expand Up @@ -284,6 +286,10 @@ template<typename Sorter>
struct verge_adapter;
```

When wrapped into [`stable_adapter`][stable-adapter], it has a slightly different behaviour: it detects strictly descending runs instead of non-ascending ones, and wraps the fallback sorter with `stable_t`. The *resulting sorter* is stable, and faster than just using `make_stable`.

*New in version 1.9.0:* explicit specialization for `stable_adapter<verge_sorter>`.


[ctad]: https://en.cppreference.com/w/cpp/language/class_template_argument_deduction
[cycle-sort]: https://en.wikipedia.org/wiki/Cycle_sort
Expand All @@ -302,4 +308,6 @@ struct verge_adapter;
[std-sort]: https://en.cppreference.com/w/cpp/algorithm/sort
[std-sorter]: https://github.com/Morwenn/cpp-sort/wiki/Sorters#std_sorter
[std-stable-sort]: https://en.cppreference.com/w/cpp/algorithm/stable_sort
[verge-adapter]: https://github.com/Morwenn/cpp-sort/wiki/Sorter-adapters#verge_adapter
[verge-sorter]: https://github.com/Morwenn/cpp-sort/wiki/Sorters#verge_sorter
[vergesort-fallbacks]: https://github.com/Morwenn/vergesort/blob/master/fallbacks.md
4 changes: 4 additions & 0 deletions docs/Sorters.md
Expand Up @@ -388,8 +388,12 @@ Vergesort's complexity is bound either by its optimization layer or by the fallb
* When it doesn't find big runs, the complexity is bound by the fallback sorter: depending on the category of iterators you can refer to the tables of either `pdq_sorter` or `quick_merge_sorter`.
* When it does find big runs, vergesort's complexity is bound by the merging phase of its optimization layer. In such a case, `inplace_merge` is used to merge the runs: it will use additional memory if any is available, in which case vergesort is O(n log n). If there isn't much extra memory available, it may still require O(log n) extra memory (and thus raise an `std::bad_alloc` if there isn't that much memory available) in which case the complexity falls to O(n log n log log n). It should not happen that much, and the additional *log log n* factor is likely irrelevant for most real-world applications.

When wrapped into [`stable_adapter`][stable-adapter], it has a slightly different behaviour: it detects strictly descending runs instead of non-ascending ones, and wraps the fallback sorter with `stable_t`. This make the specialization stable, and faster than just using `make_stable`.

*Changed in version 1.6.0:* when sorting a collection made of bidirectional iterators, `verge_sorter` falls back to `quick_merge_sorter` instead of `quick_sorter`.

*New in version 1.9.0:* explicit specialization for `stable_adapter<verge_sorter>`.

## Type-specific sorters

The following sorters are available but will only work for some specific types instead of using a user-provided comparison function. Some of them also accept projections as long as the result of the projection can be handled by the sorter.
Expand Down
21 changes: 20 additions & 1 deletion include/cpp-sort/adapters/stable_adapter.h
Expand Up @@ -24,6 +24,7 @@
#include "../detail/checkers.h"
#include "../detail/iterator_traits.h"
#include "../detail/memory.h"
#include "../detail/sized_iterator.h"

namespace cppsort
{
Expand Down Expand Up @@ -132,6 +133,23 @@ namespace cppsort
);
}

template<
typename ForwardIterator,
typename Compare,
typename Projection,
typename Sorter
>
auto make_stable_and_sort(sized_iterator<ForwardIterator> first, difference_type_t<ForwardIterator> size,
Compare&& compare, Projection&& projection, Sorter&& sorter)
-> decltype(auto)
{
// Hack to get the stable bidirectional version of vergesort
// to work correctly without duplicating tons of code
return make_stable_and_sort(first.base(), size,
std::move(compare), std::move(projection),
std::move(sorter));
}

////////////////////////////////////////////////////////////
// make_stable_impl

Expand Down Expand Up @@ -175,7 +193,8 @@ namespace cppsort
Compare compare={}, Projection projection={}) const
-> decltype(auto)
{
return make_stable_and_sort(first, std::distance(first, last),
using std::distance; // Hack for sized_iterator
return make_stable_and_sort(first, distance(first, last),
std::move(compare), std::move(projection),
this->get());
}
Expand Down
26 changes: 19 additions & 7 deletions include/cpp-sort/adapters/verge_adapter.h
Expand Up @@ -12,6 +12,7 @@
#include <iterator>
#include <type_traits>
#include <utility>
#include <cpp-sort/adapters/stable_adapter.h>
#include <cpp-sort/sorter_facade.h>
#include <cpp-sort/sorter_traits.h>
#include <cpp-sort/utility/adapter_storage.h>
Expand All @@ -25,7 +26,7 @@ namespace cppsort

namespace detail
{
template<typename FallbackSorter>
template<typename FallbackSorter, bool Stable>
struct verge_adapter_impl:
utility::adapter_storage<FallbackSorter>
{
Expand Down Expand Up @@ -55,27 +56,38 @@ namespace cppsort
"verge_adapter requires at least random-access iterators"
);

verge::sort(std::move(first), std::move(last), last - first,
std::move(compare), std::move(projection),
this->get());
verge::sort<Stable>(std::move(first), std::move(last), last - first,
std::move(compare), std::move(projection),
this->get());
}

////////////////////////////////////////////////////////////
// Sorter traits

using iterator_category = std::random_access_iterator_tag;
using is_always_stable = std::false_type;
using is_always_stable = std::integral_constant<bool, Stable>;
};
}

template<typename FallbackSorter>
struct verge_adapter:
sorter_facade<detail::verge_adapter_impl<FallbackSorter>>
sorter_facade<detail::verge_adapter_impl<FallbackSorter, false>>
{
verge_adapter() = default;

constexpr explicit verge_adapter(FallbackSorter sorter):
sorter_facade<detail::verge_adapter_impl<FallbackSorter>>(std::move(sorter))
sorter_facade<detail::verge_adapter_impl<FallbackSorter, false>>(std::move(sorter))
{}
};

template<typename FallbackSorter>
struct stable_adapter<verge_adapter<FallbackSorter>>:
sorter_facade<detail::verge_adapter_impl<FallbackSorter, true>>
{
stable_adapter() = default;

constexpr explicit stable_adapter(verge_adapter<FallbackSorter> sorter):
sorter_facade<detail::verge_adapter_impl<FallbackSorter, true>>(std::move(sorter))
{}
};
}
Expand Down
13 changes: 13 additions & 0 deletions include/cpp-sort/detail/quick_merge_sort.h
Expand Up @@ -17,6 +17,7 @@
#include "iterator_traits.h"
#include "nth_element.h"
#include "quicksort.h"
#include "sized_iterator.h"
#include "swap_ranges.h"

namespace cppsort
Expand Down Expand Up @@ -149,6 +150,18 @@ namespace detail
}
small_sort(first, last, size, std::move(compare), std::move(projection));
}

template<typename ForwardIterator, typename Compare, typename Projection>
auto quick_merge_sort(sized_iterator<ForwardIterator> first, sized_iterator<ForwardIterator> last,
difference_type_t<ForwardIterator> size,
Compare compare, Projection projection)
-> void
{
// Hack to get the stable bidirectional version of vergesort
// to work correctly without duplicating tons of code
quick_merge_sort(first.base(), last.base(), size,
std::move(compare), std::move(projection));
}
}}

#endif // CPPSORT_DETAIL_QUICK_MERGE_SORT_H_
106 changes: 106 additions & 0 deletions include/cpp-sort/detail/sized_iterator.h
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2020 Morwenn
* SPDX-License-Identifier: MIT
*/
#ifndef CPPSORT_DETAIL_SIZED_ITERATOR_H_
#define CPPSORT_DETAIL_SIZED_ITERATOR_H_

////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////

namespace cppsort
{
namespace detail
{
////////////////////////////////////////////////////////////
// Mostly a hack to avoid some gratuitous performance loss
// by passing bidirectional iterators + size to a function
// accepting a pair of iterators. It is worse than the
// equivalent C++20 features, but should be good enough for
// the internal use we make of it.
//
// NOTE: the full iterator features are not provided, this
// is intentional to avoid unintentional uses of the
// class in the library's internals.

template<typename Iterator>
class sized_iterator
{
public:

////////////////////////////////////////////////////////////
// Public types

using iterator_category = iterator_category_t<Iterator>;
using iterator_type = Iterator;
using value_type = value_type_t<Iterator>;
using difference_type = difference_type_t<Iterator>;
using pointer = pointer_t<Iterator>;
using reference = reference_t<Iterator>;

////////////////////////////////////////////////////////////
// Constructors

sized_iterator() = default;

constexpr sized_iterator(Iterator it, difference_type size):
_it(std::move(it)),
_size(size)
{}

////////////////////////////////////////////////////////////
// Members access

auto base() const
-> iterator_type
{
return _it;
}

auto size() const
-> difference_type
{
return _size;
}

////////////////////////////////////////////////////////////
// Element access

auto operator*() const
-> reference
{
return *_it;
}

auto operator->() const
-> pointer
{
return &(operator*());
}

private:

Iterator _it;
difference_type _size;
};

// Alternative to std::distance meant to be picked up by ADL in
// specific places, uses the size of the *second* iterator
template<typename Iterator>
constexpr auto distance(sized_iterator<Iterator>, sized_iterator<Iterator> last)
-> difference_type_t<Iterator>
{
return last.size();
}

template<typename Iterator>
auto make_sized_iterator(Iterator it, difference_type_t<Iterator> size)
-> sized_iterator<Iterator>
{
return { it, size };
}

}}

#endif // CPPSORT_DETAIL_SIZED_ITERATOR_H_

0 comments on commit affe28b

Please sign in to comment.