[PATCH v2] libstdc++: Improve std::lock algorithm
Jonathan Wakely
jwakely@redhat.com
Tue Jun 22 12:51:26 GMT 2021
On Tue, 22 Jun 2021 at 10:07, Matthias Kretz wrote:
>
> On Monday, 21 June 2021 19:31:59 CEST Jonathan Wakely via Gcc-patches wrote:
> > + // Lock the last element of the tuple, after all previous ones are
> > locked. + template<int _Idx, typename... _Lockables>
> > + inline __enable_if_t<_Idx + 1 == sizeof...(_Lockables), int>
> > + __try_lock_impl(tuple<_Lockables&...>& __lockables)
>
> Couldn't you drop the need for enable_if and tuple if you define the function
> like this? (Or - without constexpr if - two overloads with
> __try_lock_impl(_L1& __l1) and __try_lock_impl(_L1& __l1, _L2& __l2,
> _Lockables&... __lockables)
>
> template<typename _L1, typename... _Lockables>
> inline int
> __try_lock_impl(_L1& __l1, _Lockables&... __lockables)
> {
> if (auto __lock = __detail::__try_to_lock(__l1))
> {
> if constexpr (sizeof...(_Lockables))
> {
> int __idx = __detail::__try_lock_impl(__lockables...);
> if (__idx >= 0)
> return __idx + 1;
> }
> __lock.release();
> return -1;
> }
> else
> return 0;
> }
Yes, I did try something like that, but we can't use if-constexpr
unconditionally. Doing it with two overloads still needed enable_if or
tag dispatching, but that's because I retained the use of std::tuple,
so was passing around the whole parameter pack. With your suggestion
to also drop std::tuple the number of parameters decides which
function we call. And we don't instantiate std::tuple. And we can also
get rid of the __try_to_lock function, which was only used to deduce
the lock type rather than use tuple_element to get it. That's much
nicer.
>
> > [...]
> > + template<typename _L0, typename... _L1>
> > + void
> > + __lock_impl(int& __i, int __depth, _L0& __l0, _L1&... __l1)
> > + {
>
> How about optimizing a likely common case where all lockables have the same
> type? In that case we don't require recursion and can manage stack usage much
> simpler:
The stack usage is bounded by the number of mutexes being locked,
which is unlikely to get large, but we can do that.
We can do it for try_lock too:
template<typename _L1, typename _L2, typename... _L3>
int
try_lock(_L1& __l1, _L2& __l2, _L3&... __l3)
{
#if __cplusplus >= 201703L
if constexpr (is_same_v<_L1, _L2>
&& (is_same_v<_L1, _L3> && ...))
{
constexpr int _Np = 2 + sizeof...(_L3);
unique_lock<_L1> __locks[_Np] = {
{__l1, try_to_lock}, {__l2, try_to_lock}, {__l3, try_to_lock}...
};
for (int __i = 0; __i < _Np; ++__i)
if (!__locks[__i])
return __i;
for (auto& __l : __locks)
__l.release();
return -1;
}
else
#endif
return __detail::__try_lock_impl(__l1, __l2, __l3...);
}
>
> if constexpr ((is_same_v<_L0, _L1> && ...))
> {
> constexpr int _Np = 1 + sizeof...(_L1);
> std::array<unique_lock<_L0>, _Np> __locks = {
> {__l0, defer_lock}, {__l1, defer_lock}...
> };
> int __first = 0;
> do {
> __locks[__first].lock();
> for (int __j = 1; __j < _Np; ++__j)
> {
> const int __idx = (__first + __j) % _Np;
> if (!__locks[__idx].try_lock())
> {
> for (int __k = __idx; __k != __first;
> __k = __k == 1 ? _Np : __k - 1)
> __locks[__k - 1].unlock();
This loop doesn't work if any try_lock fails when first==0, because
the loop termination condition is never reached.
I find this a bit easier to understand than the loop above, and
correct (I think):
for (int __k = __j; __k != 0; --__k)
__locks[(__first + __k - 1) % _Np].unlock();
I'll finish testing the attached patch. I should probably add more
tests, so that each test is run for a set of lockables of the same
type, and also for lockables of different types.
-------------- next part --------------
commit 7d7cf35ed3c4b9d8c7fc4a52b4c7b1788c85c46d
Author: Jonathan Wakely <jwakely@redhat.com>
Date: Tue Jun 22 13:35:19 2021
libstdc++: Simplify std::try_lock and std::lock further
The std::try_lock and std::lock algorithms can use iteration instead of
recursion when all lockables have the same type and can be held by an
array of unique_lock<L> objects.
By making this change to __detail::__try_lock_impl it also benefits
__detail::__lock_impl, which uses it. For std::lock we can just put the
iterative version directly in std::lock, to avoid making any call to
__detail::__lock_impl.
Signed-off-by: Matthias Kretz <m.kretz@gsi.de>
Signed-off-by: Jonathan Wakely <jwakely@redhat.com>
Co-authored-by: Matthias Kretz <m.kretz@gsi.de>
libstdc++-v3/ChangeLog:
* include/std/mutex (lock): Replace recursion with iteration
when lockables all have the same type.
(__detail::__try_lock_impl): Likewise. Pass lockables as
parameters, instead of a tuple. Always lock the first one, and
recurse for the rest.
(__detail::__lock_impl): Adjust call to __try_lock_impl.
(__detail::__try_to_lock): Remove.
diff --git a/libstdc++-v3/include/std/mutex b/libstdc++-v3/include/std/mutex
index 5f2d8f9ee7b..d1ba0c95397 100644
--- a/libstdc++-v3/include/std/mutex
+++ b/libstdc++-v3/include/std/mutex
@@ -514,39 +514,53 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
/// @cond undocumented
namespace __detail
{
+ // Lock the last lockable, after all previous ones are locked.
template<typename _Lockable>
- inline unique_lock<_Lockable>
- __try_to_lock(_Lockable& __l)
- { return unique_lock<_Lockable>{__l, try_to_lock}; }
-
- // Lock the last element of the tuple, after all previous ones are locked.
- template<int _Idx, typename... _Lockables>
- inline __enable_if_t<_Idx + 1 == sizeof...(_Lockables), int>
- __try_lock_impl(tuple<_Lockables&...>& __lockables)
+ inline int
+ __try_lock_impl(_Lockable& __lockable)
{
- if (auto __lock = __detail::__try_to_lock(std::get<_Idx>(__lockables)))
+ if (unique_lock<_Lockable> __lock{__lockable, try_to_lock})
{
__lock.release();
return -1;
}
else
- return _Idx;
+ return 0;
}
- // Lock tuple elements starting from _Idx.
- template<int _Idx, typename... _Lockables>
- inline __enable_if_t<_Idx + 1 != sizeof...(_Lockables), int>
- __try_lock_impl(tuple<_Lockables&...>& __lockables)
+ // Lock each lockable in turn.
+ template<typename _L0, typename... _Lockables>
+ inline int
+ __try_lock_impl(_L0& __l0, _Lockables&... __lockables)
{
- if (auto __lock = __detail::__try_to_lock(std::get<_Idx>(__lockables)))
+#if __cplusplus >= 201703L
+ if constexpr ((is_same_v<_L0, _Lockables> && ...))
{
- int __idx = __detail::__try_lock_impl<_Idx + 1>(__lockables);
- if (__idx == -1)
- __lock.release();
- return __idx;
+ constexpr int _Np = 1 + sizeof...(_Lockables);
+ unique_lock<_L0> __locks[_Np] = {
+ {__l0, try_to_lock}, {__lockables, try_to_lock}...
+ };
+ for (int __i = 0; __i < _Np; ++__i)
+ if (!__locks[__i])
+ return __i;
+ for (auto& __l : __locks)
+ __l.release();
+ return -1;
}
else
- return _Idx;
+#endif
+ if (unique_lock<_L0> __lock{__l0, try_to_lock})
+ {
+ int __idx = __detail::__try_lock_impl(__lockables...);
+ if (__idx == -1)
+ {
+ __lock.release();
+ return -1;
+ }
+ return __idx + 1;
+ }
+ else
+ return 0;
}
} // namespace __detail
@@ -562,12 +576,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
*
* Sequentially calls try_lock() on each argument.
*/
- template<typename _Lock1, typename _Lock2, typename... _Lock3>
+ template<typename _L1, typename _L2, typename... _L3>
int
- try_lock(_Lock1& __l1, _Lock2& __l2, _Lock3&... __l3)
+ try_lock(_L1& __l1, _L2& __l2, _L3&... __l3)
{
- auto __lockables = std::tie(__l1, __l2, __l3...);
- return __detail::__try_lock_impl<0>(__lockables);
+ return __detail::__try_lock_impl(__l1, __l2, __l3...);
}
/// @cond undocumented
@@ -589,8 +602,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
int __failed = 1; // index that couldn't be locked
{
unique_lock<_L0> __first(__l0);
- auto __rest = std::tie(__l1...);
- __failed += __detail::__try_lock_impl<0>(__rest);
+ __failed += __detail::__try_lock_impl(__l1...);
if (!__failed)
{
__i = -1; // finished
@@ -620,15 +632,45 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
* @post All arguments are locked.
*
* All arguments are locked via a sequence of calls to lock(), try_lock()
- * and unlock(). If the call exits via an exception any locks that were
- * obtained will be released.
+ * and unlock(). If this function exits via an exception any locks that
+ * were obtained will be released.
*/
template<typename _L1, typename _L2, typename... _L3>
void
lock(_L1& __l1, _L2& __l2, _L3&... __l3)
{
- int __i = 0;
- __detail::__lock_impl(__i, 0, __l1, __l2, __l3...);
+#if __cplusplus >= 201703L
+ if constexpr (is_same_v<_L1, _L2> && (is_same_v<_L1, _L3> && ...))
+ {
+ constexpr int _Np = 2 + sizeof...(_L3);
+ unique_lock<_L1> __locks[] = {
+ {__l1, defer_lock}, {__l2, defer_lock}, {__l3, defer_lock}...
+ };
+ int __first = 0;
+ do {
+ __locks[__first].lock();
+ for (int __j = 1; __j < _Np; ++__j)
+ {
+ const int __idx = (__first + __j) % _Np;
+ if (!__locks[__idx].try_lock())
+ {
+ for (int __k = __j; __k != 0; --__k)
+ __locks[(__first + __k - 1) % _Np].unlock();
+ __first = __idx;
+ break;
+ }
+ }
+ } while (!__locks[__first]);
+
+ for (auto& __l : __locks)
+ __l.release();
+ }
+ else
+#endif
+ {
+ int __i = 0;
+ __detail::__lock_impl(__i, 0, __l1, __l2, __l3...);
+ }
}
#if __cplusplus >= 201703L
More information about the Libstdc++
mailing list