From 339e9bc29ca2621969d4234be7d9fc8321541a1e Mon Sep 17 00:00:00 2001 From: Mathieu David Date: Sun, 17 May 2026 20:16:36 +0200 Subject: [PATCH] perf(generator): use memcpy paths for primitive sequence conversion For UnboundedSequence fields, route both conversion directions through single-memcpy paths instead of element-by-element loops. - from_rmw_message: emit msg.field.into() instead of msg.field.into_iter().collect(). Routes through rosidl_runtime_rs' Copy-bounded From> for Vec -> as_slice().to_vec() -> memcpy. The old path paid a per-element read + zero-write through SequenceIterator::next(). - into_rmw_message Owned arm: emit msg.field.as_slice().into() instead of msg.field.into(). Routes through From<&[T]> for Sequence -> Sequence::new + clone_from_slice -> memcpy (stdlib specializes clone_from_slice for T: Copy). The old path went through From> -> from_iter -> extend, paying a per-element IndexMut write. The Borrowed arm of into_rmw_message already used as_slice().into() and is unchanged. Non-primitive element types (String, WString, nested messages) still use .into_iter().map(...).collect() -- they need real per-element conversion regardless. Also restructures the from_rmw_message UnboundedSequence branch so each element-type arm emits a single complete expression rather than sharing a chained .into_iter()...collect() across arms. Microbenchmarks (Sequence 65536 <-> Vec): receive: 23.3 us -> 513 ns (45.4x) publish: 28.3 us -> 726 ns (39.0x) End-to-end (UInt8MultiArray, single-process, reliable QoS, rmw_fastrtps_cpp, 64 KiB, median of 3 x 5s): rclrs (main): 30,578 msg/s rclrs (this branch): 135,560 msg/s (4.4x over main, ~90% of rclcpp) rclcpp baseline: 151,279 msg/s rclrs + Worker sub: 224,576 msg/s (~1.48x of rclcpp single-process) Requires rosidl_runtime_rs with the new Copy-bounded From> for Vec impl (companion PR). Refs ros2-rust/ros2_rust#628 improvement (1). --- .../resource/templates/msg_idiomatic.rs.em | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/rosidl_generator_rs/resource/templates/msg_idiomatic.rs.em b/rosidl_generator_rs/resource/templates/msg_idiomatic.rs.em index 5ed0394..0172c68 100644 --- a/rosidl_generator_rs/resource/templates/msg_idiomatic.rs.em +++ b/rosidl_generator_rs/resource/templates/msg_idiomatic.rs.em @@ -139,8 +139,12 @@ impl rosidl_runtime_rs::Message for @(type_name) { .into_iter() .map(|elem| @(get_rs_type(member.type.value_type))::into_rmw_message(std::borrow::Cow::Owned(elem)).into_owned()) .collect(), +@# Owned BasicType sequences: borrow the slice and route through +@# From<&[T]> for Sequence so the copy compiles to a memcpy via +@# clone_from_slice's Copy specialization, instead of From>'s +@# per-element extend path. See ros2-rust/ros2_rust#628. @[ else]@ - @(get_rs_name(member.name)): msg.@(get_rs_name(member.name)).into(), + @(get_rs_name(member.name)): msg.@(get_rs_name(member.name)).as_slice().into(), @[ end if]@ @# @# @@ -243,14 +247,26 @@ impl rosidl_runtime_rs::Message for @(type_name) { @# @# == UnboundedSequence == @[ elif isinstance(member.type, UnboundedSequence)]@ +@# Primitive sequences use the From> for Vec impl (memcpy via +@# as_slice().to_vec()), avoiding O(n) per-element read/zero-write that the +@# SequenceIterator path performs. See ros2-rust/ros2_rust#628. +@[ if isinstance(member.type.value_type, BasicType)]@ + @(get_rs_name(member.name)): msg.@(get_rs_name(member.name)).into(), +@[ elif isinstance(member.type.value_type, UnboundedString) or isinstance(member.type.value_type, UnboundedWString)]@ @(get_rs_name(member.name)): msg.@(get_rs_name(member.name)) .into_iter() -@[ if isinstance(member.type.value_type, UnboundedString) or isinstance(member.type.value_type, UnboundedWString)]@ .map(|elem| elem.to_string()) + .collect(), @[ elif isinstance(member.type.value_type, NamedType) or isinstance(member.type.value_type, NamespacedType)]@ + @(get_rs_name(member.name)): msg.@(get_rs_name(member.name)) + .into_iter() .map(@(get_rs_type(member.type.value_type))::from_rmw_message) -@[ end if]@ .collect(), +@[ else]@ + @(get_rs_name(member.name)): msg.@(get_rs_name(member.name)) + .into_iter() + .collect(), +@[ end if]@ @# @# @# == UnboundedString + UnboundedWString ==