Skip to content

perf: rewrite the Recalculate*Checksum inner loop over a wide one's-complement sum - #42

Merged
wiresock merged 3 commits into
v3.7.1from
perf/wide-inet-checksum
Jul 29, 2026
Merged

perf: rewrite the Recalculate*Checksum inner loop over a wide one's-complement sum#42
wiresock merged 3 commits into
v3.7.1from
perf/wide-inet-checksum

Conversation

@wiresock

@wiresock wiresock commented Jul 29, 2026

Copy link
Copy Markdown
Owner

What

All four CNdisApi::Recalculate*Checksum functions (IP, ICMP, TCP, UDP) shared one inner loop: two bytes per iteration, byte loads, shift+mask, accumulating big-endian 16-bit words. This replaces it with inet_checksum::sum16_be — a wide one's-complement sum accumulating 32-bit loads into four independent uint64_t accumulators.

Measured on an out-of-order x64 core (QueryThreadCycleTime, 1330 B TCP segments): 0.21–0.26 cycles/byte versus the old loop's 1.09–1.67 — a 5.1–6.4× speedup. On a VPN data path that recomputes L4 checksums on every packet in both directions, the old loop was the single largest per-byte cost outside the cipher.

Why it is safe

The result is bit-identical, not approximately equal:

  • The Internet checksum is byte-order independent up to one byte swap of the folded result (RFC 1071 §2(B)), so the hot loop can accumulate native little-endian words and swap only the final 16 bits.
  • The 0x0000-versus-0xFFFF folding boundary cannot diverge: a folded sum is zero only for an all-zero span in both formulations, and every nonzero case is forced by congruence mod 0xFFFF.
  • A uint64_t absorbs 2^32 32-bit additions, so the loop needs no carry handling and no intrinsics — it compiles unchanged for x86, x64 and ARM64 (all three verified).

Proven by an exhaustive differential test against the old loop — 8 alignments × 3001 lengths plus boundary fills (0x00/0xFF, lengths 0/1/2/3 and up to 65515) — run under AddressSanitizer with allocations sized so any read past the span faults. A pinned regression suite lives in the consuming client repo (kp_pkf_client_ut/inet_checksum_test.cpp), testing against a frozen verbatim copy of the old loop.

Two behavioural fixes that ride along

  1. The odd-length pad-byte write is gone. The old loop wrote a zero byte into the packet buffer one past the payload, then read it back. sum16_be uses a masked tail load and never writes anywhere. Nothing depended on the write — but note for anyone byte-diffing captures across builds: one receiver-ignored Ethernet-trailer byte can now differ on odd-L4 frames.

  2. RecalculateICMPChecksum gains the length validation its TCP and UDP siblings received in 20aa90f. It computed ntohs(ip_len) - ip_hl*4 unchecked, so a forged or offload-mangled ip_len underflowed the length and drove the checksum loop — and, worse, the pad-byte write — up to ~64 KB past m_IBuffer. The guard is the exact shape of the TCP/UDP one. The only sensitive in-tree caller pattern (synthesizing ICMP unreachable from an over-MTU frame) passes the guard: the synthesized ip_len is ≤ 96 while m_Length still holds the larger original at call time.

Testing

  • Exhaustive differential vs the old implementation under ASan (above)
  • Builds clean: x64, Win32, ARM64
  • Consuming client (kp_pkf_client): 28/28 unit tests, including the new pinned checksum suite

…omplement sum

All four functions shared one loop: two bytes per iteration, byte loads,
shift+mask, front-end bound at roughly 1.1-1.7 cycles/byte. On a VPN data path
that recomputes L4 checksums on every packet in both directions, that loop was
the single largest per-byte cost outside the cipher.

inet_checksum::sum16_be accumulates 32-bit loads into four independent u64
accumulators - a u64 absorbs 2^32 such additions, so the loop needs no carry
handling and no intrinsics, and builds unchanged for x86, x64 and ARM64.
Measured on one out-of-order x64 core: 0.21-0.26 cycles/byte, 5.1-6.4x the old
loop. The result is BIT-IDENTICAL: the Internet checksum is byte-order
independent up to one byte swap of the folded result (RFC 1071 section 2(B)),
and the 0x0000-versus-0xFFFF folding boundary cannot diverge because a folded
sum is zero only for an all-zero span in both formulations. Proven by an
exhaustive differential test against the old loop - 8 alignments x 3001
lengths plus boundary fills, under AddressSanitizer with allocations sized so
any read past the span faults.

Odd lengths are handled with a masked tail load. The old loop instead wrote a
zero pad byte INTO the packet buffer one past the payload and read it back;
nothing depended on that write, but it means raw capture bytes can differ from
old builds in one receiver-ignored Ethernet-trailer byte on odd-L4 frames.

RecalculateICMPChecksum also gains the length validation its TCP and UDP
siblings received earlier: it computed ntohs(ip_len) - ip_hl*4 unchecked, so a
forged or offload-mangled ip_len underflowed the length and drove the checksum
loop - and previously the pad-byte write - far past m_IBuffer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Copilot AI 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.

Pull request overview

This PR refactors CNdisApi::Recalculate*Checksum (IP/ICMP/TCP/UDP) to use a new wide one’s-complement accumulation routine (inet_checksum::sum16_be) to significantly speed up checksum recomputation, while also removing the prior odd-length “pad-byte write” behavior and adding missing ICMP length validation.

Changes:

  • Replace the byte-pair checksum inner loops with inet_checksum::sum16_be in IP/ICMP/TCP/UDP recalculation.
  • Remove the odd-length pad-byte write into the packet buffer by switching to a masked-tail load approach.
  • Add IP/ICMP length validation in RecalculateICMPChecksum consistent with existing TCP/UDP guards.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
ndisapi/ndisapi.cpp Switches checksum accumulation to inet_checksum::sum16_be and adds ICMP length validation while removing pad-byte writes.
ndisapi/inet_checksum.h Introduces a new wide Internet-checksum accumulation helper used by the recalculation routines.
Comments suppressed due to low confidence (2)

ndisapi/inet_checksum.h:68

  • If you add the INET_CHECKSUM_MEMCPY compatibility macro (so older standard libraries that don't expose std::memcpy still compile), the 16-byte loop should use it consistently.
            std::memcpy(&w0, data + i, 4);
            std::memcpy(&w1, data + i + 4, 4);
            std::memcpy(&w2, data + i + 8, 4);
            std::memcpy(&w3, data + i + 12, 4);

ndisapi/inet_checksum.h:78

  • Same compatibility concern for std::memcpy in the 4-byte tail loop: use the INET_CHECKSUM_MEMCPY macro consistently so the header compiles on older MSVC/STL combinations.
            std::memcpy(&w, data + i, 4);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ndisapi/inet_checksum.h
Comment thread ndisapi/inet_checksum.h Outdated
ndisapi.cpp is compiled by ndisapi.vc6/ndisapi.dsp and the VS2012 projects as
well as the modern ones, and the rest of the file guards its C++11 use behind
_MSC_VER checks. The new header ignored that: <cstdint> requires VS2010 and
`noexcept` VS2015.

Pre-VS2010 toolchains now get typedefs onto the compiler's built-in __int64
plus <string.h>'s memcpy, and pre-VS2015 an empty noexcept macro
(INET_CHECKSUM_NOEXCEPT). The body already avoided the other VC6 traps -- no
`ull` literal suffixes, no per-loop redeclaration of the index variable -- and
the one 64-bit mask constant relies on promotion (`sum & 0xFFFFFFFFu` against
a 64-bit operand) rather than a suffix.

INET_CHECKSUM_FORCE_LEGACY_COMPAT forces a modern compiler down the legacy
branch, which is how that branch is actually verified without a VC6
installation: the exhaustive differential test (8 alignments x 3001 lengths
plus boundary fills, under AddressSanitizer) passes identically through both
branches. What this does NOT prove is VC6's own parser accepting the file;
the constructs used are all VC6-era (namespaces, inline, static_cast,
__int64), but nothing here has been run through a real VC6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

ndisapi/inet_checksum.h:43

  • In the non-legacy branch, inet_checksum.h uses size_t in the sum16_be signature/body but doesn’t include a header that guarantees size_t is declared. Depending on the standard library/compile mode, relying on <cstring> for size_t can fail. Add an explicit <stddef.h> (or <cstddef>) include in the modern branch to make this header self-sufficient.
#include <cstdint>
#include <cstring>
#define INET_CHECKSUM_MEMCPY ::std::memcpy

sum16_be's signature uses unqualified size_t, which the legacy branch got from
<stddef.h> while the modern branch relied on <cstring> injecting it into the
global namespace - which the standard leaves unspecified ([support.c.headers]);
only std::size_t is guaranteed there. Every MSVC/libstdc++/libc++ in practice
does inject it, so this was latent, but the header claims self-sufficiency and
should have it. <stddef.h> now sits above the branch, covering both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wiresock

Copy link
Copy Markdown
Owner Author

For the record, the three comments Copilot suppressed as low-confidence were reviewed as well:

  • Both round-1 suppressed comments (use INET_CHECKSUM_MEMCPY consistently in the 16-byte block and the 4-byte tail loop) reviewed the pre-compat revision and were already addressed by 447f7dd — every memcpy site in the header goes through the macro.
  • The round-2 suppressed comment (modern branch relies on <cstring> for global size_t, which the standard leaves unspecified — only std::size_t is guaranteed) is technically correct and fixed in fbe17d2: <stddef.h> now sits above the branch split, guaranteeing ::size_t for both. Latent rather than live — every MSVC/libstdc++/libc++ injects it in practice — but the header claims self-sufficiency, so it should hold without relying on that.

Both compat branches re-verified after the change: the exhaustive ASan differential (8 alignments × 3001 lengths + boundary fills) passes bit-identically through the modern and forced-legacy paths, and the consuming client's pinned regression suite stays green.

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@wiresock
wiresock merged commit 684c425 into v3.7.1 Jul 29, 2026
1 check passed
@wiresock
wiresock deleted the perf/wide-inet-checksum branch July 29, 2026 20:06
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