Rb_tree constructor optimization
François Dumont
frs.dumont@gmail.com
Thu May 3 20:11:00 GMT 2018
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
More information about the Libstdc++
mailing list