[PATCH 1/4] libstdc++: Further simplify _Hashtable inheritance hierarchy
Jonathan Wakely
jwakely@redhat.com
Mon Dec 16 14:26:40 GMT 2024
I've pushed this patch series now, and I hope I'm done with
refactoring _Hashtable.
I see about a 2% reduction in memory and in compilation time for an
explicit instantiation of std::unordered_list<std::string> when
comparing r15-5031-gdd08cdccc36d08 to current trunk. It's not huge,
but it's something, and I find the code easier to understand now.
On Fri, 13 Dec 2024 at 23:07, Jonathan Wakely <jwakely@redhat.com> wrote:
>
> The main change here is using [[no_unique_address]] instead of the Empty
> Base-class Optimization. Using the attribute allows us to use data
> members instead of base-classes. That simplifies the inheritance
> hierarchy, which means less work for the compiler. It also means that
> ADL has fewer associated classes and associated namespaces to consider,
> further reducing the work the compiler has to do.
>
> Reducing the differences between the _Hashtable_ebo_helper primary
> template and the partial specialization means we no longer need to use
> member functions to access the stored object, because it's now always a
> data member called _M_obj. This means we can also remove a number of
> other helper functions that were using those member functions to access
> the object, for example we can swap the _Hash and _Equal objects
> directly in _Hashtable::swap instead of calling _Hashtable_base::_M_swap
> which then calls _Hash_code_base::_M_swap.
>
> Although [[no_unique_address]] would allow us to reduce the size for
> empty types that are also 'final', doing so would be an ABI break
> because those types were previously excluded from using the EBO. So we
> still need the _Hashtable_ebo_helper class template and a partial
> specialization, so that we only use the attribute under exactly the same
> conditions as we previously used the EBO. This could be avoided with a
> non-standard [[no_unique_address(expr)]] attribute that took a boolean
> condition, or with reflection and token sequence injection, but we don't
> have either of those things.
>
> Because _Hashtable_ebo_helper is no longer used as a base-class we don't
> need to disambiguate possible identical bases, so it doesn't need an
> integral non-type template parameter.
>
> libstdc++-v3/ChangeLog:
>
> * include/bits/hashtable.h (_Hashtable::swap): Swap hash
> function and equality predicate here. Inline allocator swap
> instead of using __alloc_on_swap.
> * include/bits/hashtable_policy.h (_Hashtable_ebo_helper):
> Replace EBO with no_unique_address attribute. Remove NTTP.
> (_Hash_code_base): Replace base class with data member using
> no_unique_address attribute.
> (_Hash_code_base::_M_swap): Remove.
> (_Hash_code_base::_M_hash): Remove.
> (_Hashtable_base): Replace base class with data member using
> no_unique_address attribute.
> (_Hashtable_base::_M_swap): Remove.
> (_Hashtable_alloc): Replace base class with data member using
> no_unique_address attribute.
> ---
>
> Tested x86_64-linux.
>
> libstdc++-v3/include/bits/hashtable.h | 16 ++-
> libstdc++-v3/include/bits/hashtable_policy.h | 126 +++++--------------
> 2 files changed, 42 insertions(+), 100 deletions(-)
>
> diff --git a/libstdc++-v3/include/bits/hashtable.h b/libstdc++-v3/include/bits/hashtable.h
> index b8bd8c2f418..2dc24985d58 100644
> --- a/libstdc++-v3/include/bits/hashtable.h
> +++ b/libstdc++-v3/include/bits/hashtable.h
> @@ -1829,12 +1829,18 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> noexcept(__and_<__is_nothrow_swappable<_Hash>,
> __is_nothrow_swappable<_Equal>>::value)
> {
> - // The only base class with member variables is hash_code_base.
> - // We define _Hash_code_base::_M_swap because different
> - // specializations have different members.
> - this->_M_swap(__x);
> + using std::swap;
> + swap(__hash_code_base::_M_hash._M_obj,
> + __x.__hash_code_base::_M_hash._M_obj);
> + swap(__hashtable_base::_M_equal._M_obj,
> + __x.__hashtable_base::_M_equal._M_obj);
> +
> +#pragma GCC diagnostic push
> +#pragma GCC diagnostic ignored "-Wc++17-extensions" // if constexpr
> + if constexpr (__node_alloc_traits::propagate_on_container_swap::value)
> + swap(this->_M_node_allocator(), __x._M_node_allocator());
> +#pragma GCC diagnostic pop
>
> - std::__alloc_on_swap(this->_M_node_allocator(), __x._M_node_allocator());
> std::swap(_M_rehash_policy, __x._M_rehash_policy);
>
> // Deal properly with potentially moved instances.
> diff --git a/libstdc++-v3/include/bits/hashtable_policy.h b/libstdc++-v3/include/bits/hashtable_policy.h
> index 6769399bd4d..8b3b7ba2682 100644
> --- a/libstdc++-v3/include/bits/hashtable_policy.h
> +++ b/libstdc++-v3/include/bits/hashtable_policy.h
> @@ -1015,46 +1015,24 @@ namespace __detail
> /**
> * Primary class template _Hashtable_ebo_helper.
> *
> - * Helper class using EBO when it is not forbidden (the type is not
> - * final) and when it is worth it (the type is empty.)
> + * Helper class using [[no_unique_address]] to reduce object size.
> */
> - template<int _Nm, typename _Tp,
> + template<typename _Tp,
> bool __use_ebo = !__is_final(_Tp) && __is_empty(_Tp)>
> - struct _Hashtable_ebo_helper;
> -
> - /// Specialization using EBO.
> - template<int _Nm, typename _Tp>
> - struct _Hashtable_ebo_helper<_Nm, _Tp, true>
> - : private _Tp
> + struct _Hashtable_ebo_helper
> {
> - _Hashtable_ebo_helper() noexcept(noexcept(_Tp())) : _Tp() { }
> -
> - template<typename _OtherTp>
> - _Hashtable_ebo_helper(_OtherTp&& __tp)
> - : _Tp(std::forward<_OtherTp>(__tp))
> - { }
> -
> - const _Tp& _M_cget() const { return static_cast<const _Tp&>(*this); }
> - _Tp& _M_get() { return static_cast<_Tp&>(*this); }
> + [[__no_unique_address__]] _Tp _M_obj;
> };
>
> - /// Specialization not using EBO.
> - template<int _Nm, typename _Tp>
> - struct _Hashtable_ebo_helper<_Nm, _Tp, false>
> +#if ! _GLIBCXX_INLINE_VERSION
> + // For ABI compatibility reasons, [[no_unique_address]] is only used
> + // for empty non-final types.
> + template<typename _Tp>
> + struct _Hashtable_ebo_helper<_Tp, false>
> {
> - _Hashtable_ebo_helper() = default;
> -
> - template<typename _OtherTp>
> - _Hashtable_ebo_helper(_OtherTp&& __tp)
> - : _M_tp(std::forward<_OtherTp>(__tp))
> - { }
> -
> - const _Tp& _M_cget() const { return _M_tp; }
> - _Tp& _M_get() { return _M_tp; }
> -
> - private:
> - _Tp _M_tp{};
> + _Tp _M_obj;
> };
> +#endif
>
> /**
> * Primary class template _Local_iterator_base.
> @@ -1067,59 +1045,39 @@ namespace __detail
> bool __cache_hash_code>
> struct _Local_iterator_base;
>
> - /**
> - * Primary class template _Hash_code_base.
> - *
> - * Encapsulates two policy issues that aren't quite orthogonal.
> - * (1) the difference between using a ranged hash function and using
> - * the combination of a hash function and a range-hashing function.
> - * In the former case we don't have such things as hash codes, so
> - * we have a dummy type as placeholder.
> - * (2) Whether or not we cache hash codes. Caching hash codes is
> - * meaningless if we have a ranged hash function.
> - *
> - * We also put the key extraction objects here, for convenience.
> - * Each specialization derives from one or more of the template
> - * parameters to benefit from Ebo. This is important as this type
> - * is inherited in some cases by the _Local_iterator_base type used
> - * to implement local_iterator and const_local_iterator. As with
> - * any iterator type we prefer to make it as small as possible.
> - */
> + // Wraps the _Hash object and provides some utility functions for using it.
> template<typename _Key, typename _Value, typename _ExtractKey,
> typename _Hash, typename _RangeHash, typename _Unused,
> - bool __cache_hash_code>
> + bool /* __cache_hash_code */>
> struct _Hash_code_base
> - : private _Hashtable_ebo_helper<1, _Hash>
> {
> - private:
> - using __ebo_hash = _Hashtable_ebo_helper<1, _Hash>;
> -
> // Gives the local iterator implementation access to _M_bucket_index().
> friend struct _Local_iterator_base<_Key, _Value, _ExtractKey,
> _Hash, _RangeHash, _Unused, false>;
> -
> public:
> - typedef _Hash hasher;
> + using hasher = _Hash;
>
> hasher
> hash_function() const
> - { return _M_hash(); }
> + { return _M_hash._M_obj; }
>
> protected:
> + [[__no_unique_address__]] _Hashtable_ebo_helper<_Hash> _M_hash{};
> +
> typedef std::size_t __hash_code;
>
> // We need the default constructor for the local iterators and _Hashtable
> // default constructor.
> _Hash_code_base() = default;
>
> - _Hash_code_base(const _Hash& __hash) : __ebo_hash(__hash) { }
> + _Hash_code_base(const _Hash& __hash) : _M_hash{__hash} { }
>
> __hash_code
> _M_hash_code(const _Key& __k) const
> {
> static_assert(__is_invocable<const _Hash&, const _Key&>{},
> "hash function must be invocable with an argument of key type");
> - return _M_hash()(__k);
> + return _M_hash._M_obj(__k);
> }
>
> template<typename _Kt>
> @@ -1128,7 +1086,7 @@ namespace __detail
> {
> static_assert(__is_invocable<const _Hash&, const _Kt&>{},
> "hash function must be invocable with an argument of key type");
> - return _M_hash()(__k);
> + return _M_hash._M_obj(__k);
> }
>
> __hash_code
> @@ -1174,16 +1132,6 @@ namespace __detail
> _M_copy_code(_Hash_node_code_cache<true>& __to,
> const _Hash_node_code_cache<true>& __from) const
> { __to._M_hash_code = __from._M_hash_code; }
> -
> - void
> - _M_swap(_Hash_code_base& __x)
> - {
> - using std::swap;
> - swap(__ebo_hash::_M_get(), __x.__ebo_hash::_M_get());
> - }
> -
> - const _Hash&
> - _M_hash() const { return __ebo_hash::_M_cget(); }
> };
>
> /// Partial specialization used when nodes contain a cached hash code.
> @@ -1461,15 +1409,13 @@ namespace __detail
> *
> * Base class templates are:
> * - __detail::_Hash_code_base
> - * - __detail::_Hashtable_ebo_helper
> */
> template<typename _Key, typename _Value, typename _ExtractKey,
> typename _Equal, typename _Hash, typename _RangeHash,
> typename _Unused, typename _Traits>
> struct _Hashtable_base
> : public _Hash_code_base<_Key, _Value, _ExtractKey, _Hash, _RangeHash,
> - _Unused, _Traits::__hash_cached::value>,
> - private _Hashtable_ebo_helper<0, _Equal>
> + _Unused, _Traits::__hash_cached::value>
> {
> public:
> typedef _Key key_type;
> @@ -1487,14 +1433,13 @@ namespace __detail
>
> using __hash_code = typename __hash_code_base::__hash_code;
>
> - private:
> - using _EqualEBO = _Hashtable_ebo_helper<0, _Equal>;
> -
> protected:
> + [[__no_unique_address__]] _Hashtable_ebo_helper<_Equal> _M_equal{};
> +
> _Hashtable_base() = default;
>
> _Hashtable_base(const _Hash& __hash, const _Equal& __eq)
> - : __hash_code_base(__hash), _EqualEBO(__eq)
> + : __hash_code_base(__hash), _M_equal{__eq}
> { }
>
> bool
> @@ -1560,27 +1505,18 @@ namespace __detail
> }
> #pragma GCC diagnostic pop
>
> - void
> - _M_swap(_Hashtable_base& __x)
> - {
> - __hash_code_base::_M_swap(__x);
> - using std::swap;
> - swap(_EqualEBO::_M_get(), __x._EqualEBO::_M_get());
> - }
> -
> const _Equal&
> - _M_eq() const { return _EqualEBO::_M_cget(); }
> + _M_eq() const noexcept { return _M_equal._M_obj; }
> };
>
> /**
> - * This type deals with all allocation and keeps an allocator instance
> - * through inheritance to benefit from EBO when possible.
> + * This type deals with all allocation and keeps an allocator instance.
> */
> template<typename _NodeAlloc>
> - struct _Hashtable_alloc : private _Hashtable_ebo_helper<0, _NodeAlloc>
> + struct _Hashtable_alloc
> {
> private:
> - using __ebo_node_alloc = _Hashtable_ebo_helper<0, _NodeAlloc>;
> + [[__no_unique_address__]] _Hashtable_ebo_helper<_NodeAlloc> _M_alloc{};
>
> template<typename>
> struct __get_value_type;
> @@ -1611,16 +1547,16 @@ namespace __detail
>
> template<typename _Alloc>
> _Hashtable_alloc(_Alloc&& __a)
> - : __ebo_node_alloc(std::forward<_Alloc>(__a))
> + : _M_alloc{std::forward<_Alloc>(__a)}
> { }
>
> __node_alloc_type&
> _M_node_allocator()
> - { return __ebo_node_alloc::_M_get(); }
> + { return _M_alloc._M_obj; }
>
> const __node_alloc_type&
> _M_node_allocator() const
> - { return __ebo_node_alloc::_M_cget(); }
> + { return _M_alloc._M_obj; }
>
> // Allocate a node and construct an element within it.
> template<typename... _Args>
> --
> 2.47.1
>
More information about the Libstdc++
mailing list