Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce segment copies #4578

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Conversation

TripleWhy
Copy link
Contributor

@TripleWhy TripleWhy commented Mar 1, 2025

Use move instead of copy where possible, avoid complexity/memory usage of full copies in other locations.

(Currently, this version would compile with deleted Segment copy functions.)

From the json/differs commit:

Move segment json-specific segment diffing to json.cpp

  • Avoid calling Segment copy constructor and destructor. This is fine
    as long as we only read primitive values.
  • Only diff stuff we are actually interested in.
  • Only diff stuff if it has an effect.

Note that the json copy code is effectively very similar to before, because the copy constructor also does a memcpy, plus additional work.

About applyValuesToSelectedSegs:
This could be simplified a lot, if we were to alter the behavior such that all selected segments would be modified unconditionally. Which would also address this function comment:

// problem: if the first selected segment already has the value to be set, other selected segments are not updated

Summary by CodeRabbit

  • Refactor

    • Streamlined LED segment update and management logic for more efficient operations and smoother effect transitions.
    • Improved logic for handling segment comparisons and updates, enhancing performance and readability.
    • Refactored segment handling to utilize move semantics, optimizing performance during segment management.
  • New Features

    • Introduced an enhanced segment state detection mechanism, improving responsiveness during configuration updates.
    • Added a new function for detailed comparison of segment attributes, enhancing difference detection during deserialization.
    • Replaced segment appending method with a more efficient in-place construction approach.

Copy link

coderabbitai bot commented Mar 1, 2025

Walkthrough

The pull request modifies segment comparison and management by removing the differs method from the Segment structure and implementing move semantics in the WS2812FX class methods. A new function is introduced in the JSON module for byte-level segment comparison, replacing the previous method. Additionally, the LED module's applyValuesToSelectedSegs function is refactored to enhance clarity and efficiency by minimizing redundant property accesses.

Changes

File(s) Change Summary
wled00/FX.h & wled00/FX_fcn.cpp Removed the differs() method from the Segment class. Updated WS2812FX methods: changed appendSegment in the header from const Segment& to template<typename... Args> for perfect forwarding and modified resetSegments in the implementation to use move semantics (via std::move(seg)) instead of copy operations.
wled00/json.cpp Added a new standalone function differs(const Segment&, const std::array<byte, sizeof(Segment)>&) in an unnamed namespace. Modified deserializeSegment to use std::memcpy for creating a backup and then comparing the segment data using the new function rather than the removed method.
wled00/led.cpp Refactored applyValuesToSelectedSegs() by storing the first selected segment's properties in local constants and using direct comparisons. This simplifies the logic by eliminating multiple data retrievals and streamlining property updates based on the stored constants.
wled00/udp.cpp Replaced the call to strip.appendSegment() with strip.emplaceSegment() in the parseNotifyPacket function, altering the method of segment management in memory while maintaining the same control flow and error handling.

Possibly related PRs

  • Soap FX optimization #4543: The changes in the main PR, which involve the removal of the differs method from the Segment class and modifications to the appendSegment method in the WS2812FX class, are directly related to the removal of the same differs method in the retrieved PR, indicating a strong connection at the code level.
✨ Finishing Touches
  • 📝 Generate Docstrings

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
wled00/json.cpp (1)

18-44: Good implementation of segment comparison without requiring copies.

This new standalone differs function provides a more efficient way to compare segments by working directly with memory representations instead of creating full copies of the segment objects. The implementation carefully checks each relevant property of the segment.

Some observations:

  1. Line 30 duplicates the palette check from line 27
  2. The bitmask 0b1111111111011110U should have a comment explaining which bits are excluded from the comparison
- if (segment.palette != segmentBackup.palette)     return true;
wled00/FX_fcn.cpp (1)

1766-1766: Optimize segment construction with move semantics.

This change optimizes the resetSegments() method by using move semantics to transfer ownership of the segment object rather than creating a potentially expensive copy. This is particularly efficient since Segment objects might contain dynamically allocated resources.

wled00/led.cpp (2)

31-43: Improved efficiency by caching segment properties.

The refactored code now uses constant references to store the properties of the first selected segment, rather than repeatedly accessing them during comparisons. This reduces function calls and potential memory operations.


44-52: Simplified loop logic with direct reference comparison.

The segment selection logic is more efficient by directly comparing references rather than calling isActive() and isSelected() for segments other than the first one. This addresses the issue mentioned in the comment on line 28, ensuring all selected segments will be updated regardless of the first segment's values.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa2949a and 9271db7.

📒 Files selected for processing (4)
  • wled00/FX.h (1 hunks)
  • wled00/FX_fcn.cpp (1 hunks)
  • wled00/json.cpp (3 hunks)
  • wled00/led.cpp (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (16)
  • GitHub Check: wled_build / Build Enviornments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Enviornments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Enviornments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Enviornments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Enviornments (esp32_eth)
  • GitHub Check: wled_build / Build Enviornments (esp32dev_V4)
  • GitHub Check: wled_build / Build Enviornments (esp32dev)
  • GitHub Check: wled_build / Build Enviornments (esp01_1m_full_compat)
  • GitHub Check: wled_build / Build Enviornments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Enviornments (nodemcuv2_compat)
  • GitHub Check: wled_build / Build Enviornments (esp01_1m_full_160)
  • GitHub Check: wled_build / Build Enviornments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Enviornments (nodemcuv2_160)
  • GitHub Check: wled_build / Build Enviornments (esp01_1m_full)
  • GitHub Check: wled_build / Build Enviornments (esp8266_2m)
  • GitHub Check: wled_build / Build Enviornments (nodemcuv2)
🔇 Additional comments (3)
wled00/FX.h (1)

885-885: Excellent move semantics optimization!

Changing the method to accept an rvalue reference (Segment &&) and using std::move() eliminates unnecessary copying when appending segments. This allows the compiler to use move operations which are more efficient for complex objects like Segment.

wled00/json.cpp (2)

65-66: Efficient backup using memcpy instead of copy construction.

Using std::memcpy to create a byte-level copy of the segment is more efficient than using the copy constructor, especially when the only purpose is to check for differences later. This avoids running constructors and destructors unnecessarily.


325-325: Using the new comparison function to detect changes.

This line replaces the previous method call to seg.differs(prev) with the new standalone function. This change maintains the same functionality while benefiting from the optimized comparison approach that doesn't require a full segment copy.

- Avoid calling Segment copy constructor and destructor. This is fine
as long as we only read primitive values.
- Only diff stuff we are actually interested in.
- Only diff stuff if it has an effect.
@TripleWhy TripleWhy force-pushed the reduceSegmentCopies branch from 0c6df89 to a0fc6bf Compare March 1, 2025 10:38
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
wled00/json.cpp (3)

18-44: Consider potential maintenance overhead of raw byte-level comparisons.
The differs() function uses reinterpret_cast and compares fields individually, but introduces a risk if Segment gains non-trivial or non-POD members. Future changes to Segment might break this comparison silently. A safer approach could either rely on specialized comparisons or enforce static_assert(std::is_trivially_copyable<Segment>::value) to ensure data consistency.


64-66: Validate that direct memcpy of Segment remains valid.
The alignas(Segment) std::array<byte, …> strategy works here, but if Segment becomes more complex (e.g., if pointers or vtables are introduced), this backup mechanism can yield unpredictable comparisons. Consider adding a compile-time check or updating documentation to protect against non-trivial changes.


325-325: Be mindful of omitted fields or pointers in the change detection.
Using differs(seg, segmentBackup) ignores some pointers (e.g., name or transient data pointers). If name changes should also trigger stateChanged, consider adding a comparison for those. Otherwise, it's consistent with the current bitmask approach.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c6df89 and a0fc6bf.

📒 Files selected for processing (4)
  • wled00/FX.h (1 hunks)
  • wled00/FX_fcn.cpp (1 hunks)
  • wled00/json.cpp (3 hunks)
  • wled00/led.cpp (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • wled00/FX.h
  • wled00/led.cpp
🔇 Additional comments (1)
wled00/FX_fcn.cpp (1)

1766-1766: Great use of move semantics for segment insertion.
Using std::move(seg) eliminates unnecessary copies and helps optimize memory usage. The approach aligns well with the PR's objective to reduce overhead.

Copy link
Collaborator

@blazoncek blazoncek left a comment

Choose a reason for hiding this comment

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

I've already fixed some of unnecessary copies by using emplace_back.
I do like differs() approach which does away with copying effect data.

@@ -28,21 +28,27 @@ void setValuesFromSegment(uint8_t s)
// problem: if the first selected segment already has the value to be set, other selected segments are not updated
void applyValuesToSelectedSegs()
{
// copy of first selected segment to tell if value was updated
unsigned firstSel = strip.getFirstSelectedSegId();
Segment selsegPrev = strip.getSegment(firstSel);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This was an oversight in original. Const reference can (and should) be used. Fixed long time ago in my fork.

Copy link
Contributor Author

@TripleWhy TripleWhy Mar 1, 2025

Choose a reason for hiding this comment

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

Judging by the comments and how it is used, that was clearly intentional. Just using a reference instead of a copy won't help here. Except if the problem function comment is something that should be addressed, see PR description.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Comment is off. There is no need to copy the segment as it si not modified in the for loop (see firstSel).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The loop iterates over all segments. When i == firstSel, the segment is not skipped, so seg, which is selsegPrev at that point in time if you use a reference, is modified, is it not?

Copy link
Collaborator

Choose a reason for hiding this comment

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

if (i == firstSel || !(seg.isActive() && seg.isSelected())) continue; that's translated if. Isn't it?
At least it should be written as this.

Copy link
Contributor Author

@TripleWhy TripleWhy Mar 2, 2025

Choose a reason for hiding this comment

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

if (i != firstSel && (!seg.isActive() || !seg.isSelected())) continue;

this is the unmodified line.

if you want to invert it, the continue would fall away or move into the else branch:

if (!(i != firstSel && (!seg.isActive() || !seg.isSelected()))) {
  // the interesting stuff: modify seg
} else {
  continue; // boring
}
   !(i != firstSel  &&  (!seg.isActive() || !seg.isSelected()))
=  !(i != firstSel) || !(!seg.isActive() || !seg.isSelected())
=   (i == firstSel) ||  ( seg.isActive() &&  seg.isSelected())

So: firstSel get's modified, no matter if it's active or selected. Additionally, other segments get modified, if they are both active and selected at the same time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Then you found a bug.

Copy link
Contributor Author

@TripleWhy TripleWhy Mar 2, 2025

Choose a reason for hiding this comment

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

I'm not so sure about that: it looks very intentional, and the comments align with it, too.

JoaDick added a commit to JoaDick/WLED that referenced this pull request Mar 1, 2025
Squashed commit of the following:

commit c4b4bf0145bacad4e75d1c5321bf4a5da26b981c
Author: Joachim Dick <[email protected]>
Date:   Sat Mar 1 18:43:47 2025 +0100

    Avoid unnecessary copy of SegmentSettings

    Based on a comment from blazoncek in this PR:
    wled#4578 (comment)

commit 74d83d87d8f302f1c264eded65b919838165ebb0
Author: Joachim Dick <[email protected]>
Date:   Sat Mar 1 18:20:17 2025 +0100

    On Segment-move-only: Replace some _segments.push_back calls with _segments.emplace_back calls

    Originally from TripleWhy:
    TripleWhy@8efc2dd

commit d8131e4c017f8d01f0f20b94c33f0f7d163afb94
Author: Joachim Dick <[email protected]>
Date:   Sat Mar 1 17:27:35 2025 +0100

    Make Segment class non-copyable and move-only

Co-Authored-By: TripleWhy <[email protected]>
Co-Authored-By: Blaž Kristan <[email protected]>
JoaDick added a commit to JoaDick/WLED that referenced this pull request Mar 1, 2025
Based on a comment from blazoncek in this PR:
wled#4578 (comment)
@@ -28,21 +28,27 @@ void setValuesFromSegment(uint8_t s)
// problem: if the first selected segment already has the value to be set, other selected segments are not updated
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me be more explicit about the following question:

Is this problem actually a problem? If so, we could simplify a lot here by addressing it.

Copy link
Collaborator

@blazoncek blazoncek Mar 2, 2025

Choose a reason for hiding this comment

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

That's the case. Consider:

setValuesFromSegment(strip.getFirstSelectedSegId());
...
applyValuesToSelectedSegs();

That's due to the ifs in the function.
Why they were written as they are is beyond my knowing. Probably as some optimisation. I just kept original logic when updating.
If all ifs are removed, so is the problem. 😄

In any case this is legacy code, seldom used in new principles.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
wled00/json.cpp (1)

18-43: Consider including pointer fields in the comparison or clarify scope.

While this byte-level comparison captures various geometry and color properties, it omits pointers such as name, data, and _t. If pointer-based fields matter for detecting changes, consider expanding the comparison or documenting that these fields remain excluded from the diff check.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8efc2dd and 9dadc06.

📒 Files selected for processing (3)
  • wled00/FX.h (1 hunks)
  • wled00/json.cpp (3 hunks)
  • wled00/udp.cpp (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (18)
  • GitHub Check: wled_build / Build Enviornments (esp32_wrover)
  • GitHub Check: wled_build / Build Enviornments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Enviornments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Enviornments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Enviornments (esp32c3dev)
  • GitHub Check: wled_build / Build Enviornments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Enviornments (esp32_eth)
  • GitHub Check: wled_build / Build Enviornments (esp32dev_V4)
  • GitHub Check: wled_build / Build Enviornments (esp32dev)
  • GitHub Check: wled_build / Build Enviornments (esp01_1m_full_compat)
  • GitHub Check: wled_build / Build Enviornments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Enviornments (nodemcuv2_compat)
  • GitHub Check: wled_build / Build Enviornments (esp01_1m_full_160)
  • GitHub Check: wled_build / Build Enviornments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Enviornments (nodemcuv2_160)
  • GitHub Check: wled_build / Build Enviornments (esp01_1m_full)
  • GitHub Check: wled_build / Build Enviornments (esp8266_2m)
  • GitHub Check: wled_build / Build Enviornments (nodemcuv2)
🔇 Additional comments (4)
wled00/udp.cpp (1)

275-275: Adopted perfect-forwarding to construct segments in place.

Switching from the old appendSegment to emplaceSegment directly constructs Segment objects without unnecessary copies, aligning well with the PR's objective to reduce memory usage. Just be mindful of potential fragmentation if multiple segments are repetitively created and removed over time.

wled00/json.cpp (2)

65-66: Verify shallow copy impact on pointer-based members.

Cloning the Segment into an std::array<byte, sizeof(Segment)> can produce a shallow snapshot of pointer fields (e.g., name, data). This is typically safe if you only track geometry/state changes, but please confirm that ignoring pointer modifications is intentional.


325-325: Logical consistency with new differs function.

Using the free function differs(seg, segmentBackup) is consistent with the removal of Segment::differs. This helps consolidate comparison logic outside the struct and avoids extra overhead. The usage here looks correct.

wled00/FX.h (1)

885-886: Efficient segment creation method.

Introducing a variadic template for emplaceSegment ensures segments are created in-place, reducing temporary copies and aligning with the PR objective to optimize memory usage. The boundary check on _segments.size() < getMaxSegments() also prevents out-of-range insertions.

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.

2 participants