Skip to content

Commit 2a65ca8

Browse files
nonassaAlan-StratCraftsAI
authored andcommitted
Fix SIMD scanner scalar tail skipping unscanned bytes near capacity
SIMD scan loops exit early when count approaches MAX to prevent array overflow, but the scalar tail started from the alignment boundary (simd_end) instead of where the SIMD loop actually stopped. This left a gap of unscanned bytes, causing missed SOH positions and incorrect truncation detection. Track actual SIMD loop position with simd_pos and resume scalar tail from there. Affected paths: scan_soh and build_index in xsimd, raw AVX2, raw AVX-512 (6 code paths total).
1 parent 3ded70b commit 2a65ca8

2 files changed

Lines changed: 39 additions & 33 deletions

File tree

include/nexusfix/parser/simd_scanner.hpp

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -171,20 +171,21 @@ inline SohPositions scan_soh_xsimd(std::span<const char> data) noexcept {
171171
const size_t simd_end = data.size() & ~(width - 1);
172172
const auto* ptr = reinterpret_cast<const uint8_t*>(data.data());
173173

174-
for (size_t i = 0; i < simd_end && result.count < MAX_SOH_POSITIONS - width; i += width) {
175-
auto chunk = xsimd::load_unaligned<Arch>(ptr + i);
174+
size_t simd_pos = 0;
175+
for (; simd_pos < simd_end && result.count < MAX_SOH_POSITIONS - width; simd_pos += width) {
176+
auto chunk = xsimd::load_unaligned<Arch>(ptr + simd_pos);
176177
auto cmp = (chunk == soh_vec);
177178
uint64_t mask = cmp.mask();
178179

179180
while (mask != 0) [[likely]] {
180181
int bit = std::countr_zero(mask);
181-
result.push(static_cast<uint16_t>(i + bit));
182+
result.push(static_cast<uint16_t>(simd_pos + bit));
182183
mask &= mask - 1;
183184
}
184185
}
185186

186-
// Scalar tail
187-
for (size_t i = simd_end; i < data.size() && result.count < MAX_SOH_POSITIONS; ++i) {
187+
// Scalar tail: resume from where SIMD loop stopped (not simd_end)
188+
for (size_t i = simd_pos; i < data.size() && result.count < MAX_SOH_POSITIONS; ++i) {
188189
if (data[i] == fix::SOH) [[unlikely]] {
189190
result.push(static_cast<uint16_t>(i));
190191
}
@@ -394,10 +395,11 @@ inline SohPositions scan_soh_avx2(std::span<const char> data) noexcept {
394395
const char* __restrict ptr = data.data();
395396

396397
// Process 32-byte chunks with AVX2
397-
for (size_t i = 0; i < simd_end && result.count < MAX_SOH_POSITIONS - 32; i += AVX2_REGISTER_SIZE) {
398+
size_t simd_pos = 0;
399+
for (; simd_pos < simd_end && result.count < MAX_SOH_POSITIONS - 32; simd_pos += AVX2_REGISTER_SIZE) {
398400
// Load 32 bytes (unaligned load is fine on modern CPUs)
399401
__m256i chunk = _mm256_loadu_si256(
400-
reinterpret_cast<const __m256i*>(ptr + i));
402+
reinterpret_cast<const __m256i*>(ptr + simd_pos));
401403

402404
// Compare with SOH
403405
__m256i cmp = _mm256_cmpeq_epi8(chunk, soh_vec);
@@ -408,13 +410,13 @@ inline SohPositions scan_soh_avx2(std::span<const char> data) noexcept {
408410
// Extract positions from bitmask (branch-free loop with early exit)
409411
while (mask != 0) [[likely]] {
410412
int bit = __builtin_ctz(mask); // Find lowest set bit
411-
result.push(static_cast<uint16_t>(i + bit));
413+
result.push(static_cast<uint16_t>(simd_pos + bit));
412414
mask &= mask - 1; // Clear lowest set bit (branch-free)
413415
}
414416
}
415417

416-
// Handle remaining bytes with scalar code
417-
for (size_t i = simd_end; i < data.size() && result.count < MAX_SOH_POSITIONS; ++i) {
418+
// Handle remaining bytes with scalar code (resume from where SIMD stopped)
419+
for (size_t i = simd_pos; i < data.size() && result.count < MAX_SOH_POSITIONS; ++i) {
418420
if (ptr[i] == fix::SOH) [[unlikely]] {
419421
result.push(static_cast<uint16_t>(i));
420422
}
@@ -552,24 +554,25 @@ inline SohPositions scan_soh_avx512(std::span<const char> data) noexcept {
552554
const char* __restrict ptr = data.data();
553555

554556
// Process 64-byte chunks with AVX-512
555-
for (size_t i = 0; i < simd_end && result.count < MAX_SOH_POSITIONS - 64; i += AVX512_REGISTER_SIZE) {
557+
size_t simd_pos = 0;
558+
for (; simd_pos < simd_end && result.count < MAX_SOH_POSITIONS - 64; simd_pos += AVX512_REGISTER_SIZE) {
556559
// Load 64 bytes (unaligned load)
557560
__m512i chunk = _mm512_loadu_si512(
558-
reinterpret_cast<const __m512i*>(ptr + i));
561+
reinterpret_cast<const __m512i*>(ptr + simd_pos));
559562

560563
// Compare with SOH - returns 64-bit mask directly (more efficient than AVX2)
561564
__mmask64 mask = _mm512_cmpeq_epi8_mask(chunk, soh_vec);
562565

563566
// Extract positions from 64-bit bitmask
564567
while (mask != 0) [[likely]] {
565568
int bit = _tzcnt_u64(mask); // Find lowest set bit (64-bit)
566-
result.push(static_cast<uint16_t>(i + bit));
569+
result.push(static_cast<uint16_t>(simd_pos + bit));
567570
mask &= mask - 1; // Clear lowest set bit
568571
}
569572
}
570573

571-
// Handle remaining bytes with scalar code
572-
for (size_t i = simd_end; i < data.size() && result.count < MAX_SOH_POSITIONS; ++i) {
574+
// Handle remaining bytes with scalar code (resume from where SIMD stopped)
575+
for (size_t i = simd_pos; i < data.size() && result.count < MAX_SOH_POSITIONS; ++i) {
573576
if (ptr[i] == fix::SOH) [[unlikely]] {
574577
result.push(static_cast<uint16_t>(i));
575578
}

include/nexusfix/parser/structural_index.hpp

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,9 @@ inline FIXStructuralIndex build_index_xsimd(std::span<const char> data) noexcept
313313
const auto* ptr = reinterpret_cast<const uint8_t*>(data.data());
314314

315315
// Process chunks
316-
for (size_t i = 0; i < simd_end && idx.soh_count < MAX_FIELDS - width; i += width) {
317-
auto chunk = xsimd::load_unaligned<Arch>(ptr + i);
316+
size_t simd_pos = 0;
317+
for (; simd_pos < simd_end && idx.soh_count < MAX_FIELDS - width; simd_pos += width) {
318+
auto chunk = xsimd::load_unaligned<Arch>(ptr + simd_pos);
318319

319320
// Detect SOH positions
320321
uint64_t soh_mask = (chunk == soh_vec).mask();
@@ -323,15 +324,15 @@ inline FIXStructuralIndex build_index_xsimd(std::span<const char> data) noexcept
323324
uint64_t eq_mask = (chunk == eq_vec).mask();
324325

325326
// Extract positions from masks
326-
extract_positions(soh_mask, i, idx.soh_positions.data(),
327+
extract_positions(soh_mask, simd_pos, idx.soh_positions.data(),
327328
idx.soh_count, MAX_FIELDS);
328-
extract_positions(eq_mask, i, idx.equals_positions.data(),
329+
extract_positions(eq_mask, simd_pos, idx.equals_positions.data(),
329330
idx.equals_count, MAX_FIELDS);
330331
}
331332

332-
// Handle remaining bytes with scalar code
333+
// Handle remaining bytes with scalar code (resume from where SIMD stopped)
333334
const char* cptr = data.data();
334-
for (size_t i = simd_end; i < data.size() && idx.soh_count < MAX_FIELDS; ++i) {
335+
for (size_t i = simd_pos; i < data.size() && idx.soh_count < MAX_FIELDS; ++i) {
335336
if (cptr[i] == fix::EQUALS) [[unlikely]] {
336337
idx.equals_positions[idx.equals_count++] = static_cast<uint16_t>(i);
337338
}
@@ -438,9 +439,10 @@ inline FIXStructuralIndex build_index_avx2(std::span<const char> data) noexcept
438439
const char* __restrict ptr = data.data();
439440

440441
// Process 32-byte chunks
441-
for (size_t i = 0; i < simd_end && idx.soh_count < MAX_FIELDS - 32; i += 32) {
442+
size_t simd_pos = 0;
443+
for (; simd_pos < simd_end && idx.soh_count < MAX_FIELDS - 32; simd_pos += 32) {
442444
__m256i chunk = _mm256_loadu_si256(
443-
reinterpret_cast<const __m256i*>(ptr + i));
445+
reinterpret_cast<const __m256i*>(ptr + simd_pos));
444446

445447
// Detect SOH positions
446448
__m256i soh_cmp = _mm256_cmpeq_epi8(chunk, soh_vec);
@@ -451,14 +453,14 @@ inline FIXStructuralIndex build_index_avx2(std::span<const char> data) noexcept
451453
uint32_t eq_mask = static_cast<uint32_t>(_mm256_movemask_epi8(eq_cmp));
452454

453455
// Extract positions from masks
454-
extract_positions_avx2(soh_mask, i, idx.soh_positions.data(),
456+
extract_positions_avx2(soh_mask, simd_pos, idx.soh_positions.data(),
455457
idx.soh_count, MAX_FIELDS);
456-
extract_positions_avx2(eq_mask, i, idx.equals_positions.data(),
458+
extract_positions_avx2(eq_mask, simd_pos, idx.equals_positions.data(),
457459
idx.equals_count, MAX_FIELDS);
458460
}
459461

460-
// Handle remaining bytes with scalar code
461-
for (size_t i = simd_end; i < data.size() && idx.soh_count < MAX_FIELDS; ++i) {
462+
// Handle remaining bytes with scalar code (resume from where SIMD stopped)
463+
for (size_t i = simd_pos; i < data.size() && idx.soh_count < MAX_FIELDS; ++i) {
462464
if (ptr[i] == fix::EQUALS) [[unlikely]] {
463465
idx.equals_positions[idx.equals_count++] = static_cast<uint16_t>(i);
464466
}
@@ -545,9 +547,10 @@ inline FIXStructuralIndex build_index_avx512(std::span<const char> data) noexcep
545547
const char* __restrict ptr = data.data();
546548

547549
// Process 64-byte chunks
548-
for (size_t i = 0; i < simd_end && idx.soh_count < MAX_FIELDS - 64; i += 64) {
550+
size_t simd_pos = 0;
551+
for (; simd_pos < simd_end && idx.soh_count < MAX_FIELDS - 64; simd_pos += 64) {
549552
__m512i chunk = _mm512_loadu_si512(
550-
reinterpret_cast<const __m512i*>(ptr + i));
553+
reinterpret_cast<const __m512i*>(ptr + simd_pos));
551554

552555
// Detect SOH positions (returns 64-bit mask directly)
553556
__mmask64 soh_mask = _mm512_cmpeq_epi8_mask(chunk, soh_vec);
@@ -556,14 +559,14 @@ inline FIXStructuralIndex build_index_avx512(std::span<const char> data) noexcep
556559
__mmask64 eq_mask = _mm512_cmpeq_epi8_mask(chunk, eq_vec);
557560

558561
// Extract positions from masks
559-
extract_positions_avx512(soh_mask, i, idx.soh_positions.data(),
562+
extract_positions_avx512(soh_mask, simd_pos, idx.soh_positions.data(),
560563
idx.soh_count, MAX_FIELDS);
561-
extract_positions_avx512(eq_mask, i, idx.equals_positions.data(),
564+
extract_positions_avx512(eq_mask, simd_pos, idx.equals_positions.data(),
562565
idx.equals_count, MAX_FIELDS);
563566
}
564567

565-
// Handle remaining bytes with scalar code
566-
for (size_t i = simd_end; i < data.size() && idx.soh_count < MAX_FIELDS; ++i) {
568+
// Handle remaining bytes with scalar code (resume from where SIMD stopped)
569+
for (size_t i = simd_pos; i < data.size() && idx.soh_count < MAX_FIELDS; ++i) {
567570
if (ptr[i] == fix::EQUALS) [[unlikely]] {
568571
idx.equals_positions[idx.equals_count++] = static_cast<uint16_t>(i);
569572
}

0 commit comments

Comments
 (0)