Skip to content

Commit 0818c05

Browse files
avikivityxemul
authored andcommitted
rpc: optimize tuple deserialization when the types are default-constructible
rpc deserialization cannot assume the types that make up the return tuple are default-constructuble, and cannot deserialize directly into the tuple constructor, so it is forced to construct a default-constructible tuple formed by wrapping every T with std::optional, deserializing into that, and then converting the temporary tuple into the return tuple by calling std::optional::value() for each element. This wrapping and unwrapping is wasteful, and while the compiler could theoretically fix everything up, in practice it does not. We notice that the first value can in fact be deserialized in the tuple constructor arguments, since there's no ordering problem for it. So we remove the std::optional wrapper for it unconditionally. For the rest of the elements, we wrap them with std::optional only if they are not default constructible. If they are, we leave them unchanged. Finally, the unwrapping process calls std::optional::value if the type was wrapped; and if none of the types were wrapped (which ought to be the common case), we return the temporary tuple without any unwrapping, reducing data movement considerably. The optimization is written in a way to also include the previous optimization when the tuple size was <= 1. Testing on ScyllaDB's messaging_service.o, we see another reduction in .text size: text data bss dec hex filename 6758116 48 236 6758400 672000 messaging_service.o.before 6741352 48 236 6741636 66de84 messaging_service.o.after About 17kB. Closes #2523
1 parent fba36a3 commit 0818c05

File tree

1 file changed

+72
-11
lines changed

1 file changed

+72
-11
lines changed

include/seastar/rpc/rpc_impl.hh

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -345,25 +345,86 @@ struct unmarshal_one {
345345
};
346346
};
347347

348+
template <typename... T>
349+
struct default_constructible_tuple_except_first;
350+
351+
template <>
352+
struct default_constructible_tuple_except_first<> {
353+
using type = std::tuple<>;
354+
};
355+
356+
template <typename T0, typename... T>
357+
struct default_constructible_tuple_except_first<T0, T...> {
358+
using type = std::tuple<
359+
T0,
360+
std::conditional_t<
361+
std::is_default_constructible_v<T>,
362+
T,
363+
std::optional<T>
364+
>...
365+
>;
366+
};
367+
368+
template <typename... T>
369+
using default_constructible_tuple_except_first_t = typename default_constructible_tuple_except_first<T...>::type;
370+
371+
// Where Tin != Tout, apply std:optional::value()
372+
template <typename... Tout, typename... Tin>
373+
auto
374+
unwrap_optional_if_needed(std::tuple<Tin...>&& tuple_in) {
375+
using tuple_in_t = std::tuple<Tin...>;
376+
using tuple_out_t = std::tuple<Tout...>;
377+
return std::invoke([&] <size_t... Idx> (std::index_sequence<Idx...>) {
378+
return tuple_out_t(
379+
std::invoke([&] () {
380+
if constexpr (std::same_as<std::tuple_element_t<Idx, tuple_in_t>, std::tuple_element_t<Idx, tuple_out_t>>) {
381+
return std::move(std::get<Idx>(tuple_in));
382+
} else {
383+
return std::move(std::get<Idx>(tuple_in).value());
384+
}
385+
})...);
386+
}, std::make_index_sequence<sizeof...(Tout)>());
387+
}
388+
348389
template <typename Serializer, typename Input, typename... T>
349390
inline std::tuple<T...> do_unmarshall(connection& c, Input& in) {
350391
// Argument order processing is unspecified, but we need to deserialize
351392
// left-to-right. So we deserialize into something that can be lazily
352393
// constructed (and can conditionally destroy itself if we only constructed some
353394
// of the arguments).
354395
//
355-
// As a special case, if the tuple has 1 or fewer elements, there is no ordering
396+
// The first element of the tuple has no ordering
356397
// problem, and we can deserialize directly into a std::tuple<T...>.
357-
if constexpr (sizeof...(T) <= 1) {
358-
return std::tuple(unmarshal_one<Serializer, Input>::template helper<T>::doit(c, in)...);
359-
} else {
360-
std::tuple<std::optional<T>...> temporary;
361-
return std::apply([&] (auto&... args) {
362-
// Comma-expression preserves left-to-right order
363-
(..., (args = unmarshal_one<Serializer, Input>::template helper<typename std::remove_reference_t<decltype(args)>::value_type>::doit(c, in)));
364-
return std::tuple(std::move(*args)...);
365-
}, temporary);
366-
}
398+
//
399+
// For the rest of the elements, if they are default-constructible, we leave
400+
// them as is, and if not, we deserialize into std::optional<T>, and later
401+
// unwrap them. If we're lucky and nothing was wrapped, we can return without
402+
// any data movement.
403+
using ret_type = std::tuple<T...>;
404+
using temporary_type = default_constructible_tuple_except_first_t<T...>;
405+
return std::invoke([&] <size_t... Idx> (std::index_sequence<Idx...>) {
406+
auto tmp = temporary_type(
407+
std::invoke([&] () -> std::tuple_element_t<Idx, temporary_type> {
408+
if constexpr (Idx == 0) {
409+
// The first T has no ordering problem, so we can deserialize it directly into the tuple
410+
return unmarshal_one<Serializer, Input>::template helper<std::tuple_element_t<Idx, ret_type>>::doit(c, in);
411+
} else {
412+
// Use default constructor for the rest of the Ts
413+
return {};
414+
}
415+
})...
416+
);
417+
// Deserialize the other Ts, comma-expression preserves left-to-right order.
418+
(void)(..., ((Idx == 0
419+
? 0
420+
: ((std::get<Idx>(tmp) = unmarshal_one<Serializer, Input>::template helper<std::tuple_element_t<Idx, ret_type>>::doit(c, in), 0)))));
421+
if constexpr (std::same_as<ret_type, temporary_type>) {
422+
// Use Named Return Vale Optimization (NVRO) if we didn't have to wrap anything
423+
return tmp;
424+
} else {
425+
return unwrap_optional_if_needed<T...>(std::move(tmp));
426+
}
427+
}, std::index_sequence_for<T...>());
367428
}
368429

369430
template <typename Serializer, typename... T>

0 commit comments

Comments
 (0)