Skip to content

Commit

Permalink
rpc: optimize tuple deserialization when the types are default-constr…
Browse files Browse the repository at this point in the history
…uctible

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.
  • Loading branch information
avikivity committed Oct 29, 2024
1 parent fba36a3 commit 35fdb3f
Showing 1 changed file with 72 additions and 11 deletions.
83 changes: 72 additions & 11 deletions include/seastar/rpc/rpc_impl.hh
Original file line number Diff line number Diff line change
Expand Up @@ -345,25 +345,86 @@ struct unmarshal_one {
};
};

template <typename... T>
struct default_constructible_tuple_except_first;

template <>
struct default_constructible_tuple_except_first<> {
using type = std::tuple<>;
};

template <typename T0, typename... T>
struct default_constructible_tuple_except_first<T0, T...> {
using type = std::tuple<
T0,
std::conditional_t<
std::is_default_constructible_v<T>,
T,
std::optional<T>
>...
>;
};

template <typename... T>
using default_constructible_tuple_except_first_t = typename default_constructible_tuple_except_first<T...>::type;

// Where Tin != Tout, apply std:optional::value()
template <typename... Tout, typename... Tin>
auto
unwrap_optional_if_needed(std::tuple<Tin...>&& tuple_in) {
using tuple_in_t = std::tuple<Tin...>;
using tuple_out_t = std::tuple<Tout...>;
return std::invoke([&] <size_t... Idx> (std::index_sequence<Idx...>) {
return tuple_out_t(
std::invoke([&] () {
if constexpr (std::same_as<std::tuple_element_t<Idx, tuple_in_t>, std::tuple_element_t<Idx, tuple_out_t>>) {
return std::move(std::get<Idx>(tuple_in));
} else {
return std::move(std::get<Idx>(tuple_in).value());
}
})...);
}, std::make_index_sequence<sizeof...(Tout)>());
}

template <typename Serializer, typename Input, typename... T>
inline std::tuple<T...> do_unmarshall(connection& c, Input& in) {
// Argument order processing is unspecified, but we need to deserialize
// left-to-right. So we deserialize into something that can be lazily
// constructed (and can conditionally destroy itself if we only constructed some
// of the arguments).
//
// As a special case, if the tuple has 1 or fewer elements, there is no ordering
// The first element of the tuple has no ordering
// problem, and we can deserialize directly into a std::tuple<T...>.
if constexpr (sizeof...(T) <= 1) {
return std::tuple(unmarshal_one<Serializer, Input>::template helper<T>::doit(c, in)...);
} else {
std::tuple<std::optional<T>...> temporary;
return std::apply([&] (auto&... args) {
// Comma-expression preserves left-to-right order
(..., (args = unmarshal_one<Serializer, Input>::template helper<typename std::remove_reference_t<decltype(args)>::value_type>::doit(c, in)));
return std::tuple(std::move(*args)...);
}, temporary);
}
//
// For the rest of the elements, if they are default-constructible, we leave
// them as is, and if not, we deserialize into std::optional<T>, and later
// unwrap them. If we're lucky and nothing was wrapped, we can return without
// any data movement.
using ret_type = std::tuple<T...>;
using temporary_type = default_constructible_tuple_except_first_t<T...>;
return std::invoke([&] <size_t... Idx> (std::index_sequence<Idx...>) {
auto tmp = temporary_type(
std::invoke([&] () -> std::tuple_element_t<Idx, temporary_type> {
if constexpr (Idx == 0) {
// The first T has no ordering problem, so we can deserialize it directly into the tuple
return unmarshal_one<Serializer, Input>::template helper<std::tuple_element_t<Idx, ret_type>>::doit(c, in);
} else {
// Use default constructor for the rest of the Ts
return {};
}
})...
);
// Deserialize the other Ts, comma-expression preserves left-to-right order.
(void)(..., ((Idx == 0
? 0
: ((std::get<Idx>(tmp) = unmarshal_one<Serializer, Input>::template helper<std::tuple_element_t<Idx, ret_type>>::doit(c, in), 0)))));
if constexpr (std::same_as<ret_type, temporary_type>) {
// Use Named Return Vale Optimization (NVRO) if we didn't have to wrap anything
return tmp;
} else {
return unwrap_optional_if_needed<T...>(std::move(tmp));
}
}, std::index_sequence_for<T...>());
}

template <typename Serializer, typename... T>
Expand Down

0 comments on commit 35fdb3f

Please sign in to comment.