On Tue, 11 Feb 2020, Patrick Palka wrote:

> This patch adds memoization for these four views so that their begin() has the
> required constant time amortized complexity.
> 
> In the general case we use std::optional to cache the result.  When the
> underlying range is a random_access_range then we store the cache as an offset
> from the beginning of the range, which should be more compact.  And when the
> underlying iterator is not copyable, then we completely disable the cache.
> 
> Using std::optional in the cache is not ideal though because it means that the
> cache can't be utilized during constexpr evaluation.  If instead of
> std::optional we store a separate flag to denote an empty cache then we'll be
> able to use the cache during constexpr evaluation at the cost of a extra byte 
> or
> so.  I am not sure which design to settle on.

Here's v2 of this patch which uses the new helper
__detail::__maybe_empty_t and provides a more descriptive commit
message.  It also refines the constraints on the partial specializations
of _CachedPosition.

-- >8 --

Subject: [PATCH] libstdc++: Memoize
 {drop,drop_while,filter,reverse}_view::begin

This patch adds memoization for these four views so that their begin() has the
required amortized constant time complexity.

In the general case we use an std::optional<iterator_t<_Range>> to cache the
iterator to the first element of the view.  But when the underlying range models
random_access_range and when sizeof(range_difference_t<_Range>) is no larger
than sizeof(iterator_t<_Range>), then we instead cache the offset to the first
element.  Otherwise, when the underlying range does not model forward_range,
then we completely disable the cache.

Additionally, in drop_view and reverse_view, we disable the cache when the
underlying range models random_access_range, because in these cases recomputing
begin() takes O(1) time anyway.

Using std::optional to cache the iterator in the general case is not ideal
though because it means that the cache can't be utilized during constexpr
evaluation.  If instead of std::optional we store an (iterator, bool) pair where
the bool denotes whether the cache is empty, then we'll be able to use the cache
during constexpr evaluation at the cost of a extra byte or so in the size of the
corresponding view structure.  I'm not sure which trade-off to choose.

libstdc++-v3/ChangeLog:

        * include/std/ranges (__detail::_CachedPosition): New struct.
        (views::filter_view::_S_needs_cached_begin): New member variable.
        (views::filter_view::_M_cached_begin): New member variable.
        (views::filter_view::begin): Use _M_cached_begin to cache its
        result.
        (views::drop_view::_S_needs_cached_begin): New static member variable.
        (views::drop_view::_M_cached_begin): New member variable.
        (views::drop_view::begin): Use _M_cached_begin to cache its result
        when _S_needs_cached_begin.
        (views::drop_while_view::_M_cached_begin): New member variable.
        (views::drop_while_view::begin): Use _M_cached_begin to cache its
        result.
        (views::reverse_view::_S_needs_cached_begin): New static member
        variable.
        (views::reverse_view::_M_cached_begin): New member variable.
        (views::reverse_view::begin): Use _M_cached_begin to cache its result
        when _S_needs_cached_begin.
        * testsuite/std/ranges/adaptors/drop.cc: Augment test to check that
        drop_view::begin caches its result.
        * testsuite/std/ranges/adaptors/drop_while.cc: Augment test to check
        that drop_while_view::begin caches its result.
        * testsuite/std/ranges/adaptors/filter.cc: Augment test to check that
        filter_view::begin caches its result.
        * testsuite/std/ranges/adaptors/reverse.cc: Augment test to check that
        reverse_view::begin caches its result.
---
 libstdc++-v3/include/std/ranges               | 136 ++++++++++++++++--
 .../testsuite/std/ranges/adaptors/drop.cc     |  57 ++++++++
 .../std/ranges/adaptors/drop_while.cc         |  38 ++++-
 .../testsuite/std/ranges/adaptors/filter.cc   |  36 +++++
 .../testsuite/std/ranges/adaptors/reverse.cc  |  56 ++++++++
 5 files changed, 308 insertions(+), 15 deletions(-)

diff --git a/libstdc++-v3/include/std/ranges b/libstdc++-v3/include/std/ranges
index 440487b15cb..e7927fb0436 100644
--- a/libstdc++-v3/include/std/ranges
+++ b/libstdc++-v3/include/std/ranges
@@ -1335,6 +1335,85 @@ namespace views
       }
   } // namespace __detail
 
+  namespace __detail
+  {
+    template<range _Range>
+      struct _CachedPosition
+      {
+      private:
+       std::optional<iterator_t<_Range>> _M_iter;
+
+      public:
+       constexpr bool
+       _M_has_value() const
+       { return _M_iter.has_value(); }
+
+       constexpr iterator_t<_Range>
+       _M_get(const _Range&) const
+       {
+         __glibcxx_assert(_M_has_value());
+         return *_M_iter;
+       }
+
+       constexpr void
+       _M_set(const _Range&, const iterator_t<_Range>& __it)
+       {
+         __glibcxx_assert(!_M_has_value());
+         if constexpr (copyable<iterator_t<_Range>>)
+           if (!is_constant_evaluated())
+             _M_iter.emplace(__it);
+       }
+      };
+
+    template<random_access_range _Range>
+      requires (sizeof(range_difference_t<_Range>)
+               <= sizeof(iterator_t<_Range>))
+      struct _CachedPosition<_Range>
+      {
+      private:
+       range_difference_t<_Range> _M_offset = -1;
+
+      public:
+       constexpr bool
+       _M_has_value() const
+       { return _M_offset >= 0; }
+
+       constexpr iterator_t<_Range>
+       _M_get(_Range& __r) const
+       {
+         __glibcxx_assert(_M_has_value());
+         return ranges::begin(__r) + _M_offset;
+       }
+
+       constexpr void
+       _M_set(_Range& __r, const iterator_t<_Range>& __it)
+       {
+         __glibcxx_assert(!_M_has_value());
+         _M_offset = __it - ranges::begin(__r);
+       }
+      };
+
+    template<range _Range>
+      requires (!forward_range<_Range>)
+      struct _CachedPosition<_Range>
+      {
+       constexpr bool
+       _M_has_value() const
+       { return false; }
+
+       constexpr iterator_t<_Range>
+       _M_get(const _Range&) const
+       {
+         __glibcxx_assert(false);
+         return {};
+       }
+
+       constexpr void
+       _M_set(const _Range&, const iterator_t<_Range>&) const
+       { }
+      };
+  } // namespace __detail
+
   template<input_range _Vp,
           indirect_unary_predicate<iterator_t<_Vp>> _Pred>
     requires view<_Vp> && is_object_v<_Pred>
@@ -1492,6 +1571,7 @@ namespace views
 
       _Vp _M_base = _Vp();
       __detail::__box<_Pred> _M_pred;
+      [[no_unique_address]] __detail::_CachedPosition<_Vp> _M_cached_begin;
 
     public:
       filter_view() = default;
@@ -1516,11 +1596,15 @@ namespace views
       constexpr _Iterator
       begin()
       {
-       // XXX: we need to cache the result here as per [range.filter.view]
+       if (_M_cached_begin._M_has_value())
+         return {*this, _M_cached_begin._M_get(_M_base)};
+
        __glibcxx_assert(_M_pred.has_value());
-       return {*this, __detail::find_if(ranges::begin(_M_base),
-                                        ranges::end(_M_base),
-                                        std::ref(*_M_pred))};
+       auto __it = __detail::find_if(ranges::begin(_M_base),
+                                     ranges::end(_M_base),
+                                     std::ref(*_M_pred));
+       _M_cached_begin._M_set(_M_base, __it);
+       return {*this, std::move(__it)};
       }
 
       constexpr auto
@@ -2121,6 +2205,10 @@ namespace views
       _Vp _M_base = _Vp();
       range_difference_t<_Vp> _M_count = 0;
 
+      static constexpr bool _S_needs_cached_begin = !random_access_range<_Vp>;
+      [[no_unique_address]]
+       __maybe_empty_t<_S_needs_cached_begin, __detail::_CachedPosition<_Vp>>;
+
     public:
       drop_view() = default;
 
@@ -2141,9 +2229,15 @@ namespace views
       begin() requires (!(__detail::__simple_view<_Vp>
                          && random_access_range<_Vp>))
       {
-       // XXX: we need to cache the result here as per [range.drop.view]
-       return ranges::next(ranges::begin(_M_base), _M_count,
-                           ranges::end(_M_base));
+       if constexpr (_S_needs_cached_begin)
+         if (_M_cached_begin._M_has_value())
+           return _M_cached_begin._M_get(_M_base);
+
+       auto __it = ranges::next(ranges::begin(_M_base),
+                                _M_count, ranges::end(_M_base));
+       if constexpr (_S_needs_cached_begin)
+         _M_cached_begin._M_set(_M_base, __it);
+       return __it;
       }
 
       constexpr auto
@@ -2199,6 +2293,7 @@ namespace views
     private:
       _Vp _M_base = _Vp();
       __detail::__box<_Pred> _M_pred;
+      [[no_unique_address]] __detail::_CachedPosition<_Vp> _M_cached_begin;
 
     public:
       drop_while_view() = default;
@@ -2223,10 +2318,14 @@ namespace views
       constexpr auto
       begin()
       {
-       // XXX: we need to cache the result here as per [range.drop.while.view]
-       return __detail::find_if_not(ranges::begin(_M_base),
-                                    ranges::end(_M_base),
-                                    std::cref(*_M_pred));
+       if (_M_cached_begin._M_has_value())
+         return _M_cached_begin._M_get(_M_base);
+
+       auto __it = __detail::find_if_not(ranges::begin(_M_base),
+                                         ranges::end(_M_base),
+                                         std::cref(*_M_pred));
+       _M_cached_begin._M_set(_M_base, __it);
+       return __it;
       }
 
       constexpr auto
@@ -3072,6 +3171,10 @@ namespace views
     private:
       _Vp _M_base = _Vp();
 
+      static constexpr bool _S_needs_cached_begin = !random_access_range<_Vp>;
+      [[no_unique_address]]
+       __maybe_empty_t<_S_needs_cached_begin, __detail::_CachedPosition<_Vp>>;
+
     public:
       reverse_view() = default;
 
@@ -3091,9 +3194,14 @@ namespace views
       constexpr reverse_iterator<iterator_t<_Vp>>
       begin()
       {
-       // XXX: we need to cache the result here as per [range.reverse.view]
-       return make_reverse_iterator(ranges::next(ranges::begin(_M_base),
-                                                 ranges::end(_M_base)));
+       if constexpr (_S_needs_cached_begin)
+         if (_M_cached_begin._M_has_value())
+           return make_reverse_iterator(_M_cached_begin._M_get(_M_base));
+
+       auto __it = ranges::next(ranges::begin(_M_base), ranges::end(_M_base));
+       if constexpr (_S_needs_cached_begin)
+         _M_cached_begin._M_set(_M_base, __it);
+       return make_reverse_iterator(std::move(__it));
       }
 
       constexpr auto
diff --git a/libstdc++-v3/testsuite/std/ranges/adaptors/drop.cc 
b/libstdc++-v3/testsuite/std/ranges/adaptors/drop.cc
index 93fbafcf5a3..2db275fb28b 100644
--- a/libstdc++-v3/testsuite/std/ranges/adaptors/drop.cc
+++ b/libstdc++-v3/testsuite/std/ranges/adaptors/drop.cc
@@ -24,6 +24,7 @@
 #include <testsuite_iterators.h>
 
 using __gnu_test::test_range;
+using __gnu_test::forward_iterator_wrapper;
 using __gnu_test::bidirectional_iterator_wrapper;
 
 namespace ranges = std::ranges;
@@ -95,6 +96,61 @@ test06()
   VERIFY( ranges::empty(x | views::drop(10)) );
 }
 
+// The following tests that drop_view::begin caches its result.
+
+template<typename T>
+struct test_wrapper : forward_iterator_wrapper<T>
+{
+  static inline int increment_count = 0;
+
+  using forward_iterator_wrapper<T>::forward_iterator_wrapper;
+
+  test_wrapper() : forward_iterator_wrapper<T>(nullptr, nullptr)
+  { }
+
+  test_wrapper
+  operator++(int)
+  {
+    auto tmp = *this;
+    ++*this;
+    return tmp;
+  }
+
+  test_wrapper&
+  operator++()
+  {
+    ++increment_count;
+    forward_iterator_wrapper<T>::operator++();
+    return *this;
+  }
+
+  test_wrapper
+  operator--(int)
+  {
+    auto tmp = *this;
+    --*this;
+    return tmp;
+  }
+
+  test_wrapper&
+  operator--()
+  {
+    forward_iterator_wrapper<T>::operator--();
+    return *this;
+  }
+};
+
+void
+test07()
+{
+  int x[] = {1,2,3,4,5};
+  test_range<int, test_wrapper> rx(x);
+  auto v = rx | views::drop(3);
+  VERIFY( ranges::equal(v, (int[]){4,5}) );
+  VERIFY( ranges::equal(v, (int[]){4,5}) );
+  VERIFY( test_wrapper<int>::increment_count == 7 );
+}
+
 int
 main()
 {
@@ -104,4 +160,5 @@ main()
   test04();
   test05();
   test06();
+  test07();
 }
diff --git a/libstdc++-v3/testsuite/std/ranges/adaptors/drop_while.cc 
b/libstdc++-v3/testsuite/std/ranges/adaptors/drop_while.cc
index be47551563d..4d8bb109fae 100644
--- a/libstdc++-v3/testsuite/std/ranges/adaptors/drop_while.cc
+++ b/libstdc++-v3/testsuite/std/ranges/adaptors/drop_while.cc
@@ -25,6 +25,8 @@
 
 using __gnu_test::test_range;
 using __gnu_test::bidirectional_iterator_wrapper;
+using __gnu_test::forward_iterator_wrapper;
+using __gnu_test::random_access_iterator_wrapper;
 
 namespace ranges = std::ranges;
 namespace views = std::ranges::views;
@@ -54,10 +56,44 @@ test02()
   static_assert(ranges::bidirectional_range<R>);
 }
 
+// The following tests that drop_while_view::begin caches its result.
+
+template<template<typename> typename wrapper>
+struct test_view : ranges::view_base
+{
+  bool begin_already_called = false;
+  static inline int x[] = {1,2,3,4,5};
+  test_range<int, wrapper> rx{x};
+
+  auto
+  begin()
+  {
+    if (begin_already_called)
+      x[0] = 10;
+    begin_already_called = true;
+    return rx.begin();
+  }
+
+  auto
+  end()
+  { return rx.end(); }
+};
+
+template<template<typename> typename wrapper>
+void
+test03()
+{
+  auto v
+    = test_view<wrapper>{} | views::drop_while([] (int i) { return i<3; });
+  VERIFY( ranges::equal(v, (int[]){3,4,5}) );
+  VERIFY( ranges::equal(v, (int[]){3,4,5}) );
+}
+
 int
 main()
 {
   test01();
   test02();
+  test03<forward_iterator_wrapper>();
+  test03<random_access_iterator_wrapper>();
 }
-
diff --git a/libstdc++-v3/testsuite/std/ranges/adaptors/filter.cc 
b/libstdc++-v3/testsuite/std/ranges/adaptors/filter.cc
index 4e41232cd5c..58d898fb207 100644
--- a/libstdc++-v3/testsuite/std/ranges/adaptors/filter.cc
+++ b/libstdc++-v3/testsuite/std/ranges/adaptors/filter.cc
@@ -25,6 +25,8 @@
 
 using __gnu_test::test_range;
 using __gnu_test::bidirectional_iterator_wrapper;
+using __gnu_test::forward_iterator_wrapper;
+using __gnu_test::random_access_iterator_wrapper;
 
 namespace ranges = std::ranges;
 namespace views = std::ranges::views;
@@ -89,6 +91,38 @@ test04()
                        (int[]){0}) );
 }
 
+// The following tests that filter_view::begin caches its result.
+
+template<template<typename> typename wrapper>
+struct test_view : ranges::view_base
+{
+  bool begin_already_called = false;
+  static inline int x[] = {1,2,3,4,5};
+  test_range<int, wrapper> rx{x};
+
+  auto
+  begin()
+  {
+    if (begin_already_called)
+      x[0] = 10;
+    begin_already_called = true;
+    return rx.begin();
+  }
+
+  auto
+  end()
+  { return rx.end(); }
+};
+
+template<template<typename> typename wrapper>
+void
+test05()
+{
+  auto v = test_view<wrapper>{} | views::filter([] (int i) { return i%2 == 0; 
});
+  VERIFY( ranges::equal(v, (int[]){2,4}) );
+  VERIFY( ranges::equal(v, (int[]){2,4}) );
+}
+
 int
 main()
 {
@@ -96,4 +130,6 @@ main()
   test02();
   test03();
   test04();
+  test05<forward_iterator_wrapper>();
+  test05<random_access_iterator_wrapper>();
 }
diff --git a/libstdc++-v3/testsuite/std/ranges/adaptors/reverse.cc 
b/libstdc++-v3/testsuite/std/ranges/adaptors/reverse.cc
index 0c6aceabbed..db85c57dd43 100644
--- a/libstdc++-v3/testsuite/std/ranges/adaptors/reverse.cc
+++ b/libstdc++-v3/testsuite/std/ranges/adaptors/reverse.cc
@@ -76,6 +76,61 @@ test04()
                        (int[]){5,4,3,2,1}) );
 }
 
+// The following tests that reverse_view::begin caches its result.
+
+template<typename T>
+struct test_wrapper : bidirectional_iterator_wrapper<T>
+{
+  static inline int increment_count = 0;
+
+  using bidirectional_iterator_wrapper<T>::bidirectional_iterator_wrapper;
+
+  test_wrapper() : bidirectional_iterator_wrapper<T>(nullptr, nullptr)
+  { }
+
+  test_wrapper
+  operator++(int)
+  {
+    auto tmp = *this;
+    ++*this;
+    return tmp;
+  }
+
+  test_wrapper&
+  operator++()
+  {
+    ++increment_count;
+    bidirectional_iterator_wrapper<T>::operator++();
+    return *this;
+  }
+
+  test_wrapper
+  operator--(int)
+  {
+    auto tmp = *this;
+    --*this;
+    return tmp;
+  }
+
+  test_wrapper&
+  operator--()
+  {
+    bidirectional_iterator_wrapper<T>::operator--();
+    return *this;
+  }
+};
+
+void
+test05()
+{
+  int x[] = {1,2,3,4,5};
+  test_range<int, test_wrapper> rx(x);
+  auto v = rx | views::reverse;
+  VERIFY( ranges::equal(v, (int[]){5,4,3,2,1}) );
+  VERIFY( ranges::equal(v, (int[]){5,4,3,2,1}) );
+  VERIFY( test_wrapper<int>::increment_count == 5 );
+}
+
 int
 main()
 {
@@ -83,4 +138,5 @@ main()
   test02();
   test03();
   test04();
+  test05();
 }
-- 
2.25.1.377.g2d2118b814

Reply via email to