@@ -167,6 +167,46 @@ pub fn calculateDifferences(allocator: *std.mem.Allocator, oldData: []const u8,
167167 const saisTimeSec = @as (f64 , @floatFromInt (saisTime )) / 1000.0 ;
168168 std .debug .print ("Suffix array built in {d:.2}s\n " , .{saisTimeSec });
169169
170+ // Build LCP array for accelerated search
171+ std .debug .print ("Building LCP array...\n " , .{});
172+ const lcpStart = std .time .milliTimestamp ();
173+
174+ // First compute PLCP (permuted LCP), then convert to LCP
175+ const plcp = try allocator .alloc (i64 , oldData .len );
176+ defer allocator .free (plcp );
177+
178+ const plcpResult = libsais .zig_libsais64_plcp (
179+ oldData .ptr ,
180+ suffixIndexes .ptr ,
181+ plcp .ptr ,
182+ oldDataLen ,
183+ );
184+
185+ if (plcpResult != 0 ) {
186+ std .debug .print ("libsais64_plcp failed with error code: {d}\n " , .{plcpResult });
187+ return error .PLCPConstructionFailed ;
188+ }
189+
190+ // LCP array: LCP[i] = length of longest common prefix between SA[i-1] and SA[i]
191+ const lcpArray = try allocator .alloc (i64 , oldData .len );
192+ defer allocator .free (lcpArray );
193+
194+ const lcpResult = libsais .zig_libsais64_lcp (
195+ plcp .ptr ,
196+ suffixIndexes .ptr ,
197+ lcpArray .ptr ,
198+ oldDataLen ,
199+ );
200+
201+ if (lcpResult != 0 ) {
202+ std .debug .print ("libsais64_lcp failed with error code: {d}\n " , .{lcpResult });
203+ return error .LCPConstructionFailed ;
204+ }
205+
206+ const lcpTime = std .time .milliTimestamp () - lcpStart ;
207+ const lcpTimeSec = @as (f64 , @floatFromInt (lcpTime )) / 1000.0 ;
208+ std .debug .print ("LCP array built in {d:.2}s\n " , .{lcpTimeSec });
209+
170210 // Add sentinel value at the end (used by the original algorithm)
171211 suffixIndexes [oldData .len ] = @intCast (oldData .len );
172212
@@ -241,6 +281,10 @@ pub fn calculateDifferences(allocator: *std.mem.Allocator, oldData: []const u8,
241281
242282 // var controlBlockOffset: usize = 0;
243283
284+ // Timing for diff phase
285+ const diffPhaseStart = std .time .milliTimestamp ();
286+ var searchCount : usize = 0 ;
287+
244288 // Begin the main loop for calculating differences
245289 while (scanIndex < newsize ) {
246290 // Update progress for logging thread
@@ -254,7 +298,9 @@ pub fn calculateDifferences(allocator: *std.mem.Allocator, oldData: []const u8,
254298 // Loop through newData, searching for matches in oldData
255299 while (scanIndex < newsize ) {
256300 // Note: most of the time during the diffing phase is spend in search()
257- matchLength = @intCast (search (suffixIndexes , oldData , newData [@intCast (scanIndex ).. ], 0 , @intCast (oldsize ), & matchPosition ));
301+ // Using LCP-accelerated search with boundary tracking for O(m + log n) instead of O(m * log n)
302+ matchLength = @intCast (searchWithLCP (suffixIndexes , lcpArray , oldData , newData [@intCast (scanIndex ).. ], 0 , @intCast (oldsize ), & matchPosition ));
303+ searchCount += 1 ;
258304
259305 // Increment matchScore based on direct matches between newData and shifted oldData
260306 while (scoreCounter < scanIndex + matchLength ) {
@@ -415,6 +461,11 @@ pub fn calculateDifferences(allocator: *std.mem.Allocator, oldData: []const u8,
415461 progressRunning = false ;
416462 progressThread .join ();
417463
464+ // Report diff phase timing
465+ const diffPhaseTime = std .time .milliTimestamp () - diffPhaseStart ;
466+ const diffPhaseSec = @as (f64 , @floatFromInt (diffPhaseTime )) / 1000.0 ;
467+ std .debug .print ("Diff phase: {d:.2}s ({d} search calls)\n " , .{ diffPhaseSec , searchCount });
468+
418469 // Tell the compression threads to wrap up and wait for them
419470 streamingBytes = false ;
420471 diffBlockThread .join ();
@@ -515,6 +566,111 @@ fn offtout(x: i64, buf: []u8) void {
515566 buf [7 ] = @intCast ((y >> 56 ) & 0xFF );
516567}
517568
569+ /// LCP-accelerated binary search to find the longest match.
570+ /// By tracking match lengths at boundaries and using the LCP array,
571+ /// we can skip redundant comparisons, achieving O(m + log n) instead of O(m * log n).
572+ fn searchWithLCP (suffixIndexes : []i64 , _ : []i64 , oldData : []const u8 , newData : []const u8 , from : usize , to : usize , bestMatchPosition : * i64 ) usize {
573+ // Use iterative approach with LCP acceleration
574+ var lo : usize = from ;
575+ var hi : usize = to ;
576+ var loMatch : usize = 0 ; // chars matching at lo boundary
577+ var hiMatch : usize = 0 ; // chars matching at hi boundary
578+
579+ const oldDataSize = oldData .len ;
580+ const newDataSize = newData .len ;
581+
582+ // Initial match lengths at boundaries
583+ loMatch = matchlenFast (oldData [@intCast (suffixIndexes [lo ]).. ], newData );
584+ hiMatch = matchlenFast (oldData [@intCast (suffixIndexes [hi ]).. ], newData );
585+
586+ var bestMatch : usize = loMatch ;
587+ bestMatchPosition .* = suffixIndexes [lo ];
588+ if (hiMatch > bestMatch ) {
589+ bestMatch = hiMatch ;
590+ bestMatchPosition .* = suffixIndexes [hi ];
591+ }
592+
593+ while (hi - lo > 1 ) {
594+ const mid = lo + (hi - lo ) / 2 ;
595+ const midSuffixPos : usize = @intCast (suffixIndexes [mid ]);
596+
597+ // Start comparison from the minimum of the two boundary matches
598+ // This is the key LCP optimization - we know at least this many chars must match
599+ const skipLen = @min (loMatch , hiMatch );
600+
601+ // Compare starting from skipLen
602+ const compareLen = @min (oldDataSize - midSuffixPos , newDataSize );
603+ var midMatch : usize = skipLen ;
604+
605+ // Continue matching from skipLen
606+ if (skipLen < compareLen ) {
607+ midMatch += matchlenFrom (oldData [midSuffixPos + skipLen .. ], newData [skipLen .. ]);
608+ }
609+
610+ // Update best match if this is better
611+ if (midMatch > bestMatch ) {
612+ bestMatch = midMatch ;
613+ bestMatchPosition .* = suffixIndexes [mid ];
614+ }
615+
616+ // Decide which half to search based on lexicographic comparison
617+ if (midMatch < compareLen and midMatch < newDataSize ) {
618+ // We stopped matching at position midMatch
619+ if (midSuffixPos + midMatch < oldDataSize and oldData [midSuffixPos + midMatch ] < newData [midMatch ]) {
620+ // Suffix at mid is less than query, search right half
621+ lo = mid ;
622+ loMatch = midMatch ;
623+ } else {
624+ // Suffix at mid is greater than query, search left half
625+ hi = mid ;
626+ hiMatch = midMatch ;
627+ }
628+ } else {
629+ // Full match up to the limit, decide based on lengths
630+ if (compareLen <= newDataSize ) {
631+ // Need longer suffixes, search left (smaller indices = longer suffixes in sorted order when equal prefix)
632+ hi = mid ;
633+ hiMatch = midMatch ;
634+ } else {
635+ lo = mid ;
636+ loMatch = midMatch ;
637+ }
638+ }
639+ }
640+
641+ return bestMatch ;
642+ }
643+
644+ /// Helper function: match length starting from an offset (for LCP-accelerated search)
645+ fn matchlenFrom (oldData : []const u8 , newData : []const u8 ) usize {
646+ const minSize = @min (oldData .len , newData .len );
647+ var i : usize = 0 ;
648+
649+ // Use 8-byte lookahead for speed
650+ while (i + 8 <= minSize ) {
651+ const oldSlice : * const [8 ]u8 = @ptrCast (& oldData [i ]);
652+ const newSlice : * const [8 ]u8 = @ptrCast (& newData [i ]);
653+ const oldAs64 = std .mem .readInt (u64 , oldSlice , std .builtin .Endian .big );
654+ const newAs64 = std .mem .readInt (u64 , newSlice , std .builtin .Endian .big );
655+
656+ if (oldAs64 != newAs64 ) {
657+ // Find exact mismatch position within the 8 bytes
658+ while (i < minSize and oldData [i ] == newData [i ]) {
659+ i += 1 ;
660+ }
661+ return i ;
662+ }
663+ i += 8 ;
664+ }
665+
666+ // Handle remaining bytes
667+ while (i < minSize and oldData [i ] == newData [i ]) {
668+ i += 1 ;
669+ }
670+
671+ return i ;
672+ }
673+
518674/// Do a binary search to find the longest match of `newData` within `oldData` using precomputed suffixIndexes.
519675fn search (suffixIndexes : []i64 , oldData : []const u8 , newData : []const u8 , from : usize , to : usize , bestMatchPosition : * i64 ) usize {
520676 var midPoint : usize = 0 ;
0 commit comments