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 <jwak...@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
>

Reply via email to