This is the mail archive of the libstdc++@gcc.gnu.org mailing list for the libstdc++ project.


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]
Other format: [Raw text]

Re: Rb_tree constructor optimization


On 02/05/2018 13:49, Jonathan Wakely wrote:
On 01/05/18 21:56 +0200, François Dumont wrote:
Hi

If not told otherwise I'll commit attached patch tomorrow.

Please do not commit it, see below.

Already discussed here:

https://gcc.gnu.org/ml/libstdc++/2017-10/msg00053.html

There's no changelog entry with the patch, so to recap, the changes
are:

- noexcept specifications are automatically deduced instead of being
 stated explicitly.
Yes, it helps to make sure rb tree code is correct and it avoids usage of non-standard _S_always_equal. We could even use std::allocator_traits in C++11.

- The allocator-extended move constructors get correct exception
 specifications in Debug Mode (consistent with normal mode). This
 should be done anyway, independent of any other changes.
Yes, I realized it after I wrote the tests to validate the noexcept qualification.

- We avoid a `if (__x._M_root() != nullptr)` branch in the case where
 the allocator-extended move constructor is used with an always-equal
 allocator, right?
Yes, this is consistent with most of the other containers doing the same.


Deducing the noexcept specifications probably has a (small?) impact on
compilation speed, as currently the allocator-extended move
constructors have:

- noexcept(is_nothrow_copy_constructible<_Compare>::value
-           && _Alloc_traits::_S_always_equal())

After the patch they have to determine the noexcept-ness of the
_Rep_type constructor taking allocator_type, which has to determine
the noexcept-ness of the _Rep_type constructor taking a
_Node_allocator, which has to determine the noexcept-ness of the
constructor it dispatches to depending on _S_always_equal(), which is
simply:

+ noexcept(is_nothrow_move_constructible<_Rb_tree_impl<_Compare>>::value)

So instead of just using that value in the first place we have to
perform three rounds of overload resolution to find the right
constructor and then check its exception specification. And then we
get the wrong answer (see below).

Compilation time for containers is already **much** slower since the
allocator-extended constructors were added for GCC 5.x, despite very
few people ever using those constructors. This patch seems to require
even more work from the compiler to parse constructors nobody uses.

I must confess that I didn't consider this aspect of the patch. I just wanted the code to be more logical. I even wonder why it wasn't done this way, now I know. Could this compilation cost be testable ? Isn't the compiler doing this work only if the constructor is being used ? Or even just only if it needs to know about the noexcept qualification ?



François


diff --git a/libstdc++-v3/include/bits/stl_map.h b/libstdc++-v3/include/bits/stl_map.h
index a4a026e..2b8fd27 100644
--- a/libstdc++-v3/include/bits/stl_map.h
+++ b/libstdc++-v3/include/bits/stl_map.h
@@ -240,8 +240,8 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER

      /// Allocator-extended move constructor.
      map(map&& __m, const allocator_type& __a)
- noexcept(is_nothrow_copy_constructible<_Compare>::value
-           && _Alloc_traits::_S_always_equal())
+      noexcept( noexcept(
+    _Rep_type(std::move(__m._M_t), declval<_Pair_alloc_type>())) )

All these calls to declval need to be qualified to avoid ADL.

It seems strange to have a mix of real values and declval expressions,
rather than:

    _Rep_type(std::declval<_Rep_type>(), std::declval<_Pair_alloc_type>())) )

    Has std::declval<>() been modified recently ? I think I tried that and it was working but after a while it started to fail so I had to replace with the other expression. I'll try it again if I eventually commit this.


      : _M_t(std::move(__m._M_t), _Pair_alloc_type(__a)) { }

      /// Allocator-extended initialier-list constructor.
diff --git a/libstdc++-v3/include/bits/stl_multimap.h b/libstdc++-v3/include/bits/stl_multimap.h
index fc8f454..b9289ab 100644
--- a/libstdc++-v3/include/bits/stl_multimap.h
+++ b/libstdc++-v3/include/bits/stl_multimap.h
@@ -237,8 +237,8 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER

      /// Allocator-extended move constructor.
      multimap(multimap&& __m, const allocator_type& __a)
- noexcept(is_nothrow_copy_constructible<_Compare>::value
-           && _Alloc_traits::_S_always_equal())
+      noexcept( noexcept(
+    _Rep_type(std::move(__m._M_t), declval<_Pair_alloc_type>())) )
      : _M_t(std::move(__m._M_t), _Pair_alloc_type(__a)) { }

      /// Allocator-extended initialier-list constructor.
diff --git a/libstdc++-v3/include/bits/stl_multiset.h b/libstdc++-v3/include/bits/stl_multiset.h
index f41f56c..afaf3b6 100644
--- a/libstdc++-v3/include/bits/stl_multiset.h
+++ b/libstdc++-v3/include/bits/stl_multiset.h
@@ -253,8 +253,8 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER

      /// Allocator-extended move constructor.
      multiset(multiset&& __m, const allocator_type& __a)
- noexcept(is_nothrow_copy_constructible<_Compare>::value
-           && _Alloc_traits::_S_always_equal())
+      noexcept( noexcept(
+    _Rep_type(std::move(__m._M_t), declval<_Key_alloc_type>())) )
      : _M_t(std::move(__m._M_t), _Key_alloc_type(__a)) { }

      /// Allocator-extended initialier-list constructor.
diff --git a/libstdc++-v3/include/bits/stl_set.h b/libstdc++-v3/include/bits/stl_set.h
index 2e332ef..fc6d1f7 100644
--- a/libstdc++-v3/include/bits/stl_set.h
+++ b/libstdc++-v3/include/bits/stl_set.h
@@ -257,8 +257,8 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER

      /// Allocator-extended move constructor.
      set(set&& __x, const allocator_type& __a)
- noexcept(is_nothrow_copy_constructible<_Compare>::value
-           && _Alloc_traits::_S_always_equal())
+      noexcept( noexcept(
+    _Rep_type(std::move(__x._M_t), declval<_Key_alloc_type>())) )
      : _M_t(std::move(__x._M_t), _Key_alloc_type(__a)) { }

      /// Allocator-extended initialier-list constructor.
diff --git a/libstdc++-v3/include/bits/stl_tree.h b/libstdc++-v3/include/bits/stl_tree.h
index d0a8448..732ac55 100644
--- a/libstdc++-v3/include/bits/stl_tree.h
+++ b/libstdc++-v3/include/bits/stl_tree.h
@@ -715,6 +715,12 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
#else
      _Rb_tree_impl(_Rb_tree_impl&&) = default;

+      _Rb_tree_impl(_Rb_tree_impl&& __x, _Node_allocator&& __a)
+      : _Node_allocator(std::move(__a)),
+        _Base_key_compare(std::move(__x)),
+        _Rb_tree_header(std::move(__x))
+      { }
+
      _Rb_tree_impl(const _Key_compare& __comp, _Node_allocator&& __a)
      : _Node_allocator(std::move(__a)), _Base_key_compare(__comp)
      { }
@@ -955,10 +961,32 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
      _Rb_tree(_Rb_tree&&) = default;

      _Rb_tree(_Rb_tree&& __x, const allocator_type& __a)
+      noexcept( noexcept(
+    _Rb_tree(std::move(__x), declval<_Node_allocator>())) )
      : _Rb_tree(std::move(__x), _Node_allocator(__a))
      { }

-      _Rb_tree(_Rb_tree&& __x, _Node_allocator&& __a);
+    private:
+      _Rb_tree(_Rb_tree&& __x, _Node_allocator&& __a, true_type)
+ noexcept(is_nothrow_move_constructible<_Rb_tree_impl<_Compare>>::value)

This is wrong.

The result of is_nothrow_move_constructible<_Rb_tree_impl<_Compare>>
depends on this constructor:

 _Rb_tree_impl(_Rb_tree_impl&&) = default;

That constructor depends on the allocator type, which might not be
noexcept, but it's undefined behaviour for it to throw. We don't want
the exception-specification to depend on the allocator, otherwise we
fail this test on the line marked XXX:

template<typename T>
struct Alloc : std::allocator<T>
{
 Alloc() { }
 Alloc(const Alloc&) { }
 template<typename U> Alloc(const Alloc<U>&) { }

 template<typename U> struct rebind { using other = Alloc<U>; };
};

template<typename A>
using Set = std::set<int, std::less<int>, A>;

template<typename A>
 using Test = std::is_nothrow_constructible<Set<A>, Set<A>, A>;

int main()
{
 static_assert(Test<std::allocator<int>>::value, "");
 static_assert(Test<Alloc<int>>::value, "");  // XXX
}

In my copy of the Standard I see no noexcept qualifications on associative container constructors, is it too old ?


The changes to deduce the exception-specifications seem to make the
code more fragile -- do we really want to make those changes?
Considering all your feedbacks I would say no. I'll rework it then.


+      : _M_impl(std::move(__x._M_impl), std::move(__a))
+      { }
+
+      _Rb_tree(_Rb_tree&& __x, _Node_allocator&& __a, false_type)
+      : _M_impl(__x._M_impl._M_key_compare, std::move(__a))
+      {
+    if (__x._M_root() != nullptr)
+      _M_move_data(__x, false_type{});
+      }
+
+    public:
+      _Rb_tree(_Rb_tree&& __x, _Node_allocator&& __a)
+      noexcept( noexcept(
+    _Rb_tree(std::move(__x), std::move(__a),
+         declval<typename _Alloc_traits::is_always_equal>()) ) )
+      : _Rb_tree(std::move(__x), std::move(__a),
+         typename _Alloc_traits::is_always_equal{})
+      { }
#endif


diff --git a/libstdc++-v3/include/debug/map.h b/libstdc++-v3/include/debug/map.h
index 414b4dc..9edb7dc 100644
--- a/libstdc++-v3/include/debug/map.h
+++ b/libstdc++-v3/include/debug/map.h
@@ -105,8 +105,10 @@ namespace __debug
      : _Base(__m, __a) { }

      map(map&& __m, const allocator_type& __a)
+      noexcept( noexcept(_Base(std::move(__m._M_base()), __a)) )
      : _Safe(std::move(__m._M_safe()), __a),
-    _Base(std::move(__m._M_base()), __a) { }
+    _Base(std::move(__m._M_base()), __a)
+      { }

This part of the patch is actually a bug fix, making the debug mode
consistent with normal mode.

diff --git a/libstdc++-v3/testsuite/23_containers/map/cons/noexcept_move_construct.cc b/libstdc++-v3/testsuite/23_containers/map/cons/noexcept_move_construct.cc
index 0041408..8791eb8 100644
--- a/libstdc++-v3/testsuite/23_containers/map/cons/noexcept_move_construct.cc +++ b/libstdc++-v3/testsuite/23_containers/map/cons/noexcept_move_construct.cc
@@ -23,4 +23,25 @@

typedef std::map<int, int> mtype;

-static_assert(std::is_nothrow_move_constructible<mtype>::value, "Error");
+static_assert( std::is_nothrow_move_constructible<mtype>::value,
+           "noexcept move constructor" );
+static_assert( noexcept( mtype(std::declval<mtype>(),
+        std::declval<const typename mtype::allocator_type&>()) ),

This can use std::is_nothrow_constructible

+           "noexcept move constructor with allocator" );
+
+struct ExceptLess

Please name this "ThrowingLess" instead of "ExceptLess", I think
that's more descriptive.

+{
+  ExceptLess() = default;
+  ExceptLess(const ExceptLess&) /* noexcept */
+  { }
+
+  bool
+  operator()(int l, int r) const
+  { return l < r; }
+};
+
+typedef std::map<int, int, ExceptLess> emtype;
+
+static_assert( !noexcept( emtype(std::declval<emtype>(),
+        std::declval<const typename emtype::allocator_type&>()) ),

This can use std::is_nothrow_constructible too.

Same comments for the other tests

Ok


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]