Skip to content

perf(portable_binary): skip byte-swap for DataSize==1 - #888

Open
type-name-T wants to merge 1 commit into
USCiLab:masterfrom
type-name-T:perf/skip-byteswap-single-byte
Open

perf(portable_binary): skip byte-swap for DataSize==1#888
type-name-T wants to merge 1 commit into
USCiLab:masterfrom
type-name-T:perf/skip-byteswap-single-byte

Conversation

@type-name-T

Copy link
Copy Markdown

Problem

When serializing a std::vector of a single-byte arithmetic type (e.g. std::vector<std::uint8_t>, std::vector<char>, std::vector<unsigned char>) through PortableBinaryArchive with endianness conversion enabled, the data still enters the byte-swapping slow path even though swapping a single byte is a no-op.

In vector.hpp, arithmetic vectors use the binary_data fast path:

ar( binary_data( vector.data(), vector.size() * sizeof(T) ) );

This dispatches to saveBinary<sizeof(T)> / loadBinary<sizeof(T)>. For sizeof(T) == 1, DataSize is 1, so:

  • swap_bytes<1> iterates 0 times — a no-op the compiler can eliminate
  • but the surrounding control flow still pays the cost:
    • saveBinary: O(n) single-byte sputn(..., 1) calls instead of one bulk sputn
    • loadBinary: O(n) empty swap-loop iterations

Fix

Gate the byte-swap path behind DataSize > 1 in both functions:

// saveBinary (include/cereal/archives/portable_binary.hpp)
if( itsConvertEndianness && DataSize > 1 )   // was: if( itsConvertEndianness )
{ /* per-byte reversed write */ }
else
  writtenSize = itsStream.rdbuf()->sputn( ... , size );   // bulk write

// loadBinary
if( itsConvertEndianness && DataSize > 1 )   // was: if( itsConvertEndianness )
{ /* swap loop */ }

Why this is safe

  • Correctness: Swapping a single byte is a no-op (swap_bytes<1> iterates 0 times), so skipping it produces byte-identical output for DataSize == 1.
  • Zero added runtime cost: DataSize is a non-type template parameter, so DataSize > 1 is a compile-time constant. Compilers eliminate the dead branch, leaving the DataSize > 1 path identical to before.
  • No behavior change for multi-byte types (uint16/32/64, float, double, etc.).

Performance impact

For std::vector<std::uint8_t> (or any 1-byte vector) serialized with endianness conversion enabled:

  • Save: collapses O(n) single-byte sputn calls into one bulk sputn of the whole buffer.
  • Load: removes O(n) empty swap-loop iterations.

Files changed

include/cereal/archives/portable_binary.hpp — 2 lines modified (both if conditions).

Single-byte types (e.g. std::vector<std::uint8_t>, std::vector<char>,
std::vector<std::byte>) have nothing to swap, but still entered the
per-byte slow path in saveBinary/loadBinary when itsConvertEndianness
was set.

- saveBinary: avoided O(n) single-byte sputn calls; now uses the bulk
  sputn branch directly.
- loadBinary: skipped the empty swap_bytes<1> loop iteration overhead.

DataSize is a non-type template parameter, so `DataSize > 1` is a
compile-time constant. The dead branch is eliminated by the optimizer,
leaving multi-byte types (uint16/32/64, float, double) with zero
added runtime cost and byte-identical behavior.
@redchairman

redchairman commented Aug 26, 2026 via email

Copy link
Copy Markdown

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