improve lookup time for matches_any_publishers()#3084
improve lookup time for matches_any_publishers()#3084fujitatomoya wants to merge 2 commits intorollingfrom
Conversation
Signed-off-by: Tomoya Fujita <Tomoya.Fujita@sony.com>
| // implementation_identifier pointer comparison is not used here because | ||
| // intra-process communication is always within the same process and RMW, | ||
| // and pointer comparison is fragile across dynamically loaded components. |
There was a problem hiding this comment.
this is new compared to #3068. i think we do not need to do the comparison against RMW identifiers.
|
|
||
| mutable std::shared_timed_mutex mutex_; | ||
|
|
||
| GidToPublisherInfoMap gid_to_publisher_info_; |
There was a problem hiding this comment.
This is the critical ABI fix.
The original placement shifted mutex_'s offset within the class, causing template methods (inlined in downstream packages like Nav2) to access the wrong memory address for mutex_. The corrupted mutex writes overwrote the hashmap's internal state, producing the SIGSEGV in _M_find_before_node.
| // Publisher weak_ptr already expired, fall back to linear scan by pub_id. | ||
| for (auto git = gid_to_publisher_info_.begin(); git != gid_to_publisher_info_.end(); ++git) { | ||
| if (git->second.pub_id == intra_process_publisher_id) { | ||
| gid_to_publisher_info_.erase(git); | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
added fall back clean-up process just in case.
There was a problem hiding this comment.
Pull request overview
This PR addresses performance overhead in rclcpp::experimental::IntraProcessManager::matches_any_publishers() (reported in #3067) by replacing the per-call scan over publishers with a constant-time lookup keyed by publisher GID.
Changes:
- Add a
rmw_gid_t-keyedunordered_map(gid_to_publisher_info_) to enable O(1) publisher-existence checks by GID. - Populate/maintain the GID map in
add_publisher()/remove_publisher()and use it inmatches_any_publishers(). - Update intra-process manager unit test mocks to provide a stable mock GID and
get_gid().
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
rclcpp/src/rclcpp/intra_process_manager.cpp |
Inserts/erases a GID→publisher mapping and switches matches_any_publishers() to a hash lookup. |
rclcpp/include/rclcpp/experimental/intra_process_manager.hpp |
Adds rmw_gid_t hashing/equality helpers and a new gid_to_publisher_info_ member type. |
rclcpp/test/rclcpp/test_intra_process_manager.cpp |
Extends mock PublisherBase with a mock GID and get_gid() to support the new lookup path in tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Equality comparison for rmw_gid_t to enable use in unordered_map | ||
| struct rmw_gid_equal | ||
| { | ||
| bool operator()(const rmw_gid_t & lhs, const rmw_gid_t & rhs) const noexcept | ||
| { | ||
| // Compare the data bytes only. | ||
| // implementation_identifier pointer comparison is not used here because | ||
| // intra-process communication is always within the same process and RMW, | ||
| // and pointer comparison is fragile across dynamically loaded components. | ||
| return std::equal( | ||
| std::begin(lhs.data), | ||
| std::end(lhs.data), | ||
| std::begin(rhs.data)); | ||
| } | ||
| }; |
There was a problem hiding this comment.
rmw_gid_equal uses std::equal, but this header does not include <algorithm>. Relying on transitive includes can break builds depending on include order; add #include <algorithm> in this header to ensure std::equal is declared.
| bool | ||
| IntraProcessManager::matches_any_publishers(const rmw_gid_t * id) const | ||
| { | ||
| std::shared_lock<std::shared_timed_mutex> lock(mutex_); | ||
|
|
||
| for (auto & publisher_pair : publishers_) { | ||
| auto publisher = publisher_pair.second.lock(); | ||
| if (!publisher) { | ||
| continue; | ||
| } | ||
| if (*publisher.get() == id) { | ||
| return true; | ||
| } | ||
| // Single O(1) hash map lookup - struct contains both ID and weak_ptr | ||
| auto it = gid_to_publisher_info_.find(*id); | ||
| if (it == gid_to_publisher_info_.end()) { | ||
| return false; |
There was a problem hiding this comment.
matches_any_publishers() dereferences id unconditionally (find(*id)), which will segfault if a caller passes nullptr. Since the API takes a pointer, consider handling nullptr explicitly (e.g., return false or throw std::invalid_argument) before dereferencing.
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
remove_publisher() only erases gid_to_publisher_info_ when the publisher id exists in publishers_. If remove_publisher() is called with an id that is missing/was already erased, the GID map entry will never be removed, leading to stale entries and unbounded growth. Consider always scanning/erasing by pub_id as a fallback even when pub_it == publishers_.end().
| } | |
| } | |
| } else { | |
| // Publisher id not found in publishers_, but there might still be a stale | |
| // entry in gid_to_publisher_info_. Fall back to linear scan by pub_id. | |
| for (auto git = gid_to_publisher_info_.begin(); git != gid_to_publisher_info_.end(); ++git) { | |
| if (git->second.pub_id == intra_process_publisher_id) { | |
| gid_to_publisher_info_.erase(git); | |
| break; | |
| } | |
| } |
| // Remove GID to publisher info mapping. | ||
| // First try via the publisher's own GID (fast path). | ||
| auto pub_it = publishers_.find(intra_process_publisher_id); | ||
| if (pub_it != publishers_.end()) { | ||
| auto publisher = pub_it->second.lock(); | ||
| if (publisher) { | ||
| gid_to_publisher_info_.erase(publisher->get_gid()); | ||
| } else { | ||
| // Publisher weak_ptr already expired, fall back to linear scan by pub_id. | ||
| for (auto git = gid_to_publisher_info_.begin(); git != gid_to_publisher_info_.end(); ++git) { | ||
| if (git->second.pub_id == intra_process_publisher_id) { | ||
| gid_to_publisher_info_.erase(git); | ||
| break; |
There was a problem hiding this comment.
The “fast path” in remove_publisher() (pub_it->second.lock() then publisher->get_gid()) is unlikely to succeed when called from PublisherBase::~PublisherBase() because the weak_ptr lock typically fails once the last shared_ptr is gone. That means removals will usually take the O(N) scan anyway. To keep removals O(1) and simplify control flow, consider storing a pub_id→gid (or pub_id→iterator) mapping at insertion time and erasing by pub_id directly.
| auto ptr_value = reinterpret_cast<std::uintptr_t>(this); | ||
| for (size_t i = 0; i < RMW_GID_STORAGE_SIZE; ++i) { |
There was a problem hiding this comment.
The mock GID initialization shifts ptr_value by (i * 8) for all i < RMW_GID_STORAGE_SIZE. When RMW_GID_STORAGE_SIZE * 8 exceeds the bit width of std::uintptr_t (common), shifting by >= width is undefined behavior. Consider copying bytes from ptr_value only up to sizeof(std::uintptr_t) and zero-filling the remaining GID bytes (or memcpy into gid_.data).
| auto ptr_value = reinterpret_cast<std::uintptr_t>(this); | |
| for (size_t i = 0; i < RMW_GID_STORAGE_SIZE; ++i) { | |
| auto ptr_value = reinterpret_cast<std::uintptr_t>(this); | |
| // Zero-fill the GID data to ensure deterministic contents | |
| for (size_t i = 0; i < RMW_GID_STORAGE_SIZE; ++i) { | |
| gid_.data[i] = 0u; | |
| } | |
| // Copy at most sizeof(std::uintptr_t) bytes from the pointer value to avoid | |
| // shifting by an amount greater than or equal to the bit width of uintptr_t. | |
| const size_t ptr_num_bytes = sizeof(std::uintptr_t); | |
| const size_t bytes_to_copy = | |
| (RMW_GID_STORAGE_SIZE < ptr_num_bytes) ? RMW_GID_STORAGE_SIZE : ptr_num_bytes; | |
| for (size_t i = 0; i < bytes_to_copy; ++i) { |
| bool | ||
| IntraProcessManager::matches_any_publishers(const rmw_gid_t * id) const | ||
| { | ||
| std::shared_lock<std::shared_timed_mutex> lock(mutex_); | ||
|
|
||
| for (auto & publisher_pair : publishers_) { | ||
| auto publisher = publisher_pair.second.lock(); | ||
| if (!publisher) { | ||
| continue; | ||
| } | ||
| if (*publisher.get() == id) { | ||
| return true; | ||
| } | ||
| // Single O(1) hash map lookup - struct contains both ID and weak_ptr | ||
| auto it = gid_to_publisher_info_.find(*id); | ||
| if (it == gid_to_publisher_info_.end()) { | ||
| return false; | ||
| } | ||
| return false; | ||
|
|
||
| // Verify the publisher still exists by checking the weak_ptr | ||
| auto publisher = it->second.publisher.lock(); | ||
| return publisher != nullptr; | ||
| } |
There was a problem hiding this comment.
This PR changes matches_any_publishers() from a linear scan to a hash-map lookup and introduces gid_to_publisher_info_ maintenance. There’s no unit test asserting that matches_any_publishers() returns true after add_publisher() and false after remove_publisher() (and that stale entries don’t cause incorrect results). Adding a focused test would help prevent regressions in the new lookup/removal logic.
mini-1235
left a comment
There was a problem hiding this comment.
Thanks @fujitatomoya! I don't have time right now to run a full performance test again, but based on CPU usage observations, I can clearly see improved performance with this PR!
|
FYI @SteveMacenski |
Description
Fixes #3067 (take 2 🎥 )
Is this user-facing behavior change?
Yes
Did you use Generative AI?
Partially with Claude Opus 4.6
Additional Information
see #3067 (comment)