Skip to content

Improve serialization/deserialization for raw bytes - #1

Merged
julianadrianheine merged 1 commit into
releases/tri/0.10.5from
julianadrianheine/improve_serialization
Jul 6, 2026
Merged

Improve serialization/deserialization for raw bytes#1
julianadrianheine merged 1 commit into
releases/tri/0.10.5from
julianadrianheine/improve_serialization

Conversation

@julianadrianheine

@julianadrianheine julianadrianheine commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Description

Improve the serialization and deserialization when handling raw bytes in cyclonedds-python

Context

When writing sequences of bytes, the CDR serializer was iterating through the data element by element using a generic slow path. We added a fast path that detects when the sequence is already a bytes object and writes it directly as a raw buffer, avoiding the per-element overhead.

What is being modified

  • cyclonedds/idl/_support.py: Two new methods added to Buffer (write_sequence and read_sequence) that use Python's built-in array module to serialize and deserialize homogeneous primitive sequences in a single bulk copy, followed by a one-shot byteswap() when wire and host endianness differ. uint8 is special-cased to bytes() for an additional speedup.
  • cyclonedds/idl/_machinery.py: PlainCdrV2ArrayOfPrimitiveMachine and PlainCdrV2SequenceOfPrimitiveMachine now delegate to the new bulk methods instead of building a length-prefixed struct format string on every call and unpacking elements one by one.

Results

The benchmark covers two sequence sizes: N=100 (small sequences, representative of small payloads) and N=100000 (large sequences, representative of high-throughput payloads). All five primitive types supported by the CDR machinery are included. Times are in µs, averaged over 300 iterations.

The largest gains appear on uint8 at large N (up to -80% ser / -70% des) due to the bytes() fast path. Multi-byte types show consistent 10–36% improvement at large N. At small N the gains are more modest since fixed per-call overhead dominates over the bulk copy savings.

PlainCdrV2ArrayOfPrimitiveMachine

type N ser (µs) des (µs)
uint8 100 1.5 → 0.8 (-47%) 0.9 → 0.7 (-22%)
uint8 100000 1127.8 → 225.3 (-80%) 476.1 → 142.6 (-70%)
uint16 100 2.3 → 1.6 (-30%) 1.4 → 1.3 (-7%)
uint16 100000 1124.5 → 817.6 (-27%) 1092.6 → 928.4 (-15%)
int32 100 2.2 → 1.7 (-23%) 1.4 → 1.4 (0%)
int32 100000 1164.6 → 822.1 (-29%) 1163.0 → 1198.3 (+3%)
float32 100 2.0 → 1.6 (-20%) 1.2 → 1.0 (-17%)
float32 100000 918.8 → 832.1 (-9%) 1165.8 → 1107.1 (-5%)
float64 100 2.3 → 1.8 (-22%) 1.1 → 1.1 (0%)
float64 100000 1500.0 → 1303.7 (-13%) 873.0 → 649.3 (-26%)

PlainCdrV2SequenceOfPrimitiveMachine

type N ser (µs) des (µs)
uint8 100 1.8 → 1.1 (-39%) 1.2 → 1.0 (-17%)
uint8 100000 887.2 → 221.1 (-75%) 246.8 → 142.8 (-42%)
uint16 100 2.6 → 1.8 (-31%) 1.6 → 1.5 (-6%)
uint16 100000 1202.5 → 817.6 (-32%) 723.4 → 585.7 (-19%)
int32 100 2.6 → 1.9 (-27%) 1.6 → 1.6 (0%)
int32 100000 1299.3 → 828.6 (-36%) 803.2 → 636.7 (-21%)
float32 100 2.4 → 1.9 (-21%) 1.4 → 1.3 (-7%)
float32 100000 1106.9 → 816.5 (-26%) 991.0 → 706.5 (-29%)
float64 100 2.7 → 2.0 (-26%) 1.5 → 1.3 (-13%)
float64 100000 1278.8 → 871.1 (-32%) 960.5 → 734.1 (-24%)

@julianadrianheine julianadrianheine changed the title bulk serialize primitive arrays via array module Improve serialization/deserialization for raw bytes Jun 29, 2026
@julianadrianheine julianadrianheine self-assigned this Jun 29, 2026
@julianadrianheine
julianadrianheine requested a review from frneer June 29, 2026 17:27
if code == 'B':
return self.write_bytes(bytes(values))
a = _array.array(code, values)
if (self.endianness == Endianness.Little) != (sys.byteorder == 'little'):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems this check is redundant, right?

See the class definition:

    self.set_endianness(Endianness.native())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It looks like even when the endianness is being initialized in the constructor, there are parts of the code that can set it again, based on the CDR encapsulation header in _main.py:

if has_header and buffer.tell() == 0:
            buffer.read('b', 1)
            v = buffer.read('b', 1)
            if (v & 1) > 0:
                buffer.set_endianness(Endianness.Little)
            else:
                buffer.set_endianness(Endianness.Big)

@@ -10,6 +10,7 @@
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Meta: Some tests are supposed to be run. Can we verify that all tests still pass CI? And add tests if needed? Should we set up a github action to run these tests?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sure, let me take a look at it

@imcmahon-tri imcmahon-tri left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for these changes! The actual code changes look great. A few points:

  • Consider separating the wheel changes separated into their own PR.
  • It would be useful to have docstrings or comments describing the necessity for the new functions.
  • Adding some unit tests which check functionality but also show a performance baseline improvement would be appreciated.

@@ -0,0 +1,78 @@
#!/usr/bin/env bash

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For clarity, could you carve out the wheel building into it's own PR? That will make it easier to refer to this PR when discussing the serialize & deserialize improvements with upstream.

self._pos += size
return v

def write_sequence(self, code: str, values) -> 'Buffer':

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The (de)serialization improvements look good. Could you add comments or docstrings to describe why these functions are necessary and perhaps how they address the byte/list issue?

a.byteswap()
return self.write_bytes(a.tobytes())

def read_sequence(self, code: str, length: int) -> list:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Have you considered adding unit tests for these new functions? This PR is making performance improvement assertions but lacks a concrete metric showing how it improves the situation. Consider adding a metric to show the read/write performance with & without the write/read_sequence functions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done! I added some tests, as well as some metrics in the PR description

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for adding these comments and tests.

@julianadrianheine
julianadrianheine force-pushed the julianadrianheine/improve_serialization branch from 4cf3954 to c6f4cd2 Compare July 3, 2026 11:39

@imcmahon-tri imcmahon-tri left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

:LGTM: / :shipit: Thanks for iterating on this. It looks great and is a very helpful explanation of the issue you solved.

a.byteswap()
return self.write_bytes(a.tobytes())

def read_sequence(self, code: str, length: int) -> list:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for adding these comments and tests.

@frneer frneer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@julianadrianheine
julianadrianheine merged commit d7fbd1c into releases/tri/0.10.5 Jul 6, 2026
2 checks passed
@julianadrianheine
julianadrianheine deleted the julianadrianheine/improve_serialization branch July 6, 2026 15:10
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.

3 participants