Skip to content

Comments

improve lookup time for matches_any_publishers()#3084

Open
fujitatomoya wants to merge 2 commits intorollingfrom
issues/3067-2
Open

improve lookup time for matches_any_publishers()#3084
fujitatomoya wants to merge 2 commits intorollingfrom
issues/3067-2

Conversation

@fujitatomoya
Copy link
Collaborator

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)

…3077)

This reverts commit 1bf4e6a.

Signed-off-by: Tomoya Fujita <Tomoya.Fujita@sony.com>
Signed-off-by: Tomoya Fujita <Tomoya.Fujita@sony.com>
Comment on lines +412 to +414
// 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.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +112 to +118
// 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;
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added fall back clean-up process just in case.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-keyed unordered_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 in matches_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.

Comment on lines +406 to +420
/// 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));
}
};
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +135
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;
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
break;
}
}
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Suggested change
}
}
} 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;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +116
// 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;
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +169
auto ptr_value = reinterpret_cast<std::uintptr_t>(this);
for (size_t i = 0; i < RMW_GID_STORAGE_SIZE; ++i) {
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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) {

Copilot uses AI. Check for mistakes.
Comment on lines 127 to 141
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;
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@mini-1235 mini-1235 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@mini-1235
Copy link
Contributor

FYI @SteveMacenski

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate performance of matches_any_publishers() when enabling intraprocess

2 participants