perf: rewrite the Recalculate*Checksum inner loop over a wide one's-complement sum - #42
Conversation
…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>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
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_bein 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
RecalculateICMPChecksumconsistent 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.
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>
There was a problem hiding this comment.
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_tin thesum16_besignature/body but doesn’t include a header that guaranteessize_tis declared. Depending on the standard library/compile mode, relying on<cstring>forsize_tcan 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>
|
For the record, the three comments Copilot suppressed as low-confidence were reviewed as well:
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. |
What
All four
CNdisApi::Recalculate*Checksumfunctions (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 withinet_checksum::sum16_be— a wide one's-complement sum accumulating 32-bit loads into four independentuint64_taccumulators.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:
0x0000-versus-0xFFFFfolding 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 mod0xFFFF.uint64_tabsorbs 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
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_beuses 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.RecalculateICMPChecksumgains the length validation its TCP and UDP siblings received in 20aa90f. It computedntohs(ip_len) - ip_hl*4unchecked, so a forged or offload-mangledip_lenunderflowed the length and drove the checksum loop — and, worse, the pad-byte write — up to ~64 KB pastm_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 synthesizedip_lenis ≤ 96 whilem_Lengthstill holds the larger original at call time.Testing