On Tue, 26 Nov 2024 at 12:22, Jan Hubicka <hubi...@ucw.cz> wrote: > > > On 24/11/24 01:42 +0100, Jan Hubicka wrote: > > > Hi, > > > another common source of unnecesary throw_bad_alloc calls is > > > basic_string::_M_create. > > > basic_string<_CharT, _Traits, _Alloc>:: > > > _M_create(size_type& __capacity, size_type __old_capacity) > > > { > > > // _GLIBCXX_RESOLVE_LIB_DEFECTS > > > // 83. String::npos vs. string::max_size() > > > if (__capacity > max_size()) > > > std::__throw_length_error(__N("basic_string::_M_create")); > > > > > > // The below implements an exponential growth policy, necessary to > > > // meet amortized linear time requirements of the library: see > > > // http://gcc.gnu.org/ml/libstdc++/2001-07/msg00085.html. > > > if (__capacity > __old_capacity && __capacity < 2 * __old_capacity) > > > { > > > __capacity = 2 * __old_capacity; > > > // Never allocate a string bigger than max_size. > > > if (__capacity > max_size()) > > > __capacity = max_size(); > > > } > > > > > > // NB: Need an array of char_type[__capacity], plus a terminating > > > // null char_type() element. > > > return _S_allocate(_M_get_allocator(), __capacity + 1); > > > } > > > > > > The code makes it visible that string is never bigger then max_size, > > > however + 1 > > > makes it one byte bigger than maximal size allowed by allocator. I > > > believe > > > this is by miscounting the 0 at the end of string in max_size function: > > > > > > - { return (_Alloc_traits::max_size(_M_get_allocator()) - 1) / 2; } > > > + { return _Alloc_traits::max_size(_M_get_allocator()) / 2 - 1; } > > > > Ah yes, good catch. That code was copied from ext/sso_string_base.h > > where it has been present since r0-76546-g3ad7074772808f in 2006! > it was caught by VRP :) > I was thinking to try to add warning attribute to that throw_bad_allocv > and see what comes out. > > Thinking of that code, I am not sure it is correct, still. I see that > allocator's max_size returns full address space and / 2 is needed to get > to ptr_diff_t range. However I think allocator is not required to > return full address space and in case it returns something smaller, > diding by 2 is not necessary.
That's true. If the allocator doesn't provide max_size() then the default is required to be size_t(-1) / sizeof(T), which I consider a defect in the standard: https://eel.is/c++draft/allocator.traits.members#8 It probably should have been PTRDIFF_MAX / sizeof(T). > In fact I think this already happen for > wstring. > > Vector's max size is: > static _GLIBCXX20_CONSTEXPR size_type > _S_max_size(const _Tp_alloc_type& __a) _GLIBCXX_NOEXCEPT > { > // std::distance(begin(), end()) cannot be greater than PTRDIFF_MAX, > // and realistically we can't store more than PTRDIFF_MAX/sizeof(T) > // (even if std::allocator_traits::max_size says we can). > const size_t __diffmax > = __gnu_cxx::__numeric_traits<ptrdiff_t>::__max / sizeof(_Tp); > const size_t __allocmax = _Alloc_traits::max_size(__a); > return (std::min)(__diffmax, __allocmax); > } > > Perhaps we want to do the same here just add -1 for the trailing 0? Sounds good. > { > const size_t __diffmax > = __gnu_cxx::__numeric_traits<ptrdiff_t>::__max / sizeof(_charT); > const size_t __allocmax = _Alloc_traits::max_size(_M_get_allocator()); > return (std::min)(__diffmax, __allocmax) - 1; > } I have no objection. It's theoretically an ABI break, because somebody could do: constexpr max_string_len = std::string().max_size(); char buf[some_function_of(max_string_len)]; But I think in practice, nobody really cares about max_size anyway. On 64-bit targets you will never be able to use anything close to that size. Anybody who has baked std::string::max_size() into their program's ABI is a fool. > > > I find it funny that the code special cases a.size () == 1 to copy single > > > byte > > > and a.size () == 0 to bypass memcpy call. I would say it is job of > > > middle-end > > > to optimize memcpy reasonably even for small blocks. Was this based on > > > some data > > > showing that many strings have size of 0 and 1 which is not visible to > > > compiler? > > > > That was added as _M_copy in 2004 by r0-62838-gec61e852bc917b and when > > I implemented the new std::__cxx11::basic_string I copied it, only > > renaming it to _S_copy because that's our convention for static member > > functions. > > > > There's no explanation in the ChangeLog (obviously, because ChangeLog > > files are useless at telling you *why* something happened), but this > > comment was added: > > > > + // When __n = 1 way faster than the general multichar > > + // traits_type::copy/move/assign. > > > > Back in 2004 char_traits<char>::copy called memcpy (not > > __builtin_memcpy) and maybe it was badly optimized. > > > > I think we can probably drop that "optimization" for __n == 1. > > If size of string is not compile-time constant, gcc by default will call > memcpy (on x86 memcpy is faster for very large memory blocks) which does > have fast path for small blocks. So the code special casing _n == 1 may > get faster for variably sized strings that are very often of size 1, but > that observation applies to many other copy operations we have around. I think strings of size one are not common :-) I think most users would prefer if their strings > 1 get faster even if it means a tiny performance loss for strings == 1. > > > > Please use __sz for these variables, as that's consistent with names > > used elsewhere. > > Updated in my tree. > > > > > + if (__siz > max_size ()) > > > + __builtin_unreachable (); > > > + return __siz; > > > + } > > > > > > /// Returns the number of characters in the string, not including > > > any > > > /// null-termination. > > > _GLIBCXX_NODISCARD _GLIBCXX20_CONSTEXPR > > > size_type > > > length() const _GLIBCXX_NOEXCEPT > > > - { return _M_string_length; } > > > + { > > > + size_type __siz = _M_string_length; > > > + if (__siz > max_size ()) > > > + __builtin_unreachable (); > > > + return __siz; > > > > Should this just call size() instead of repeating the definition? > > I also changed this to call. > > > > > + } > > > > > > /// Returns the size() of the largest possible %string. > > > _GLIBCXX_NODISCARD _GLIBCXX20_CONSTEXPR > > > size_type > > > max_size() const _GLIBCXX_NOEXCEPT > > > - { return (_Alloc_traits::max_size(_M_get_allocator()) - 1) / 2; } > > > + { return _Alloc_traits::max_size(_M_get_allocator()) / 2 - 1; } > > > > > > /** > > > * @brief Resizes the %string to the specified number of > > > characters. > > > @@ -1184,8 +1194,11 @@ _GLIBCXX_BEGIN_NAMESPACE_CXX11 > > > size_type > > > capacity() const _GLIBCXX_NOEXCEPT > > > { > > > - return _M_is_local() ? size_type(_S_local_capacity) > > > - : _M_allocated_capacity; > > > + size_t __siz = _M_is_local() ? size_type(_S_local_capacity) > > > > size_type __sz = ... > Also fixed > > > > > + : _M_allocated_capacity; > > > + if (__siz < _S_local_capacity || __siz > max_size () + 1) > > > > Is this +1 correct? > > > > I think it should be simply `__siz > max_size()` > > > > The largest allocation allowed is max_size()+1 but the value stored in > > _M_allocated_capacity will be just max_size() without the +1. > > You are right. I misunderstood the code and tought capacity includes > also the trailing 0.