Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,10 @@ internal fun inflate(
bytesWrittenToOutput = dictionaryEndIndex
}

while (bytesWrittenToOutput < copyEndIndex) {
buffer[bytesWrittenToOutput] = buffer[bytesWrittenToOutput - matchDistance]
bytesWrittenToOutput++
val remainingMatchLength = copyEndIndex - bytesWrittenToOutput
if (remainingMatchLength > 0) {
copyMatch(buffer, bytesWrittenToOutput, matchDistance, remainingMatchLength)
bytesWrittenToOutput = copyEndIndex
}
lastBitPosition = currentBitPosition
}
Expand All @@ -427,6 +428,47 @@ internal fun inflate(
return workingBuffer.copyOfRange(0, bytesWrittenToOutput)
}

internal fun copyMatch(
buffer: ByteArray,
destinationOffset: Int,
distance: Int,
length: Int,
) {
if (distance == 1) {
buffer.fill(buffer[destinationOffset - 1], destinationOffset, destinationOffset + length)
return
}

if (distance >= length) {
buffer.copyInto(
destination = buffer,
destinationOffset = destinationOffset,
startIndex = destinationOffset - distance,
endIndex = destinationOffset - distance + length,
)
return
}

buffer.copyInto(
destination = buffer,
destinationOffset = destinationOffset,
startIndex = destinationOffset - distance,
endIndex = destinationOffset,
)

var copied = distance
while (copied < length) {
val copyLength = minOf(copied, length - copied)
buffer.copyInto(
destination = buffer,
destinationOffset = destinationOffset + copied,
startIndex = destinationOffset,
endIndex = destinationOffset + copyLength,
)
copied += copyLength
}
}

internal fun deflate(
data: ByteArray,
level: Int,
Expand Down
47 changes: 47 additions & 0 deletions kflate/src/jvmTest/kotlin/MatchCopyTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.rafambn.kflate.algorithm

import kotlin.test.Test
import kotlin.test.assertContentEquals

class MatchCopyTest {

@Test
fun copiesNonOverlappingMatchInOneBulkOperation() {
val buffer = ByteArray(16)
buffer[0] = 1
buffer[1] = 2
buffer[2] = 3
buffer[3] = 4
buffer[4] = 5

copyMatch(buffer, destinationOffset = 8, distance = 8, length = 5)

assertContentEquals(byteArrayOf(1, 2, 3, 4, 5), buffer.copyOfRange(8, 13))
assertContentEquals(byteArrayOf(0, 0, 0), buffer.copyOfRange(13, 16))
}

@Test
fun expandsOverlappingMatchWithoutReadingUnwrittenBytes() {
val buffer = ByteArray(16)
buffer[0] = 1
buffer[1] = 2
buffer[2] = 3

copyMatch(buffer, destinationOffset = 3, distance = 3, length = 13)

assertContentEquals(
byteArrayOf(1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1),
buffer,
)
}

@Test
fun fillsSingleByteBackReferenceAcrossOverlap() {
val buffer = ByteArray(12)
buffer[0] = 7

copyMatch(buffer, destinationOffset = 1, distance = 1, length = 11)

assertContentEquals(ByteArray(12) { 7 }, buffer)
}
}
48 changes: 48 additions & 0 deletions performance/2026-09-09-copy/RESULTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copy inflate matches with bulk array operations

Use a fill for distance-one matches, a single copy for non-overlapping matches, and expanding copies for overlapping matches. Dictionary handling and output-limit checks remain at the call site. Compression output is unchanged.

Base `fe2ff51` on `dev-1.1.0`. Measured September 9, 2026 on Linux x86_64, Intel Core i7-11800H. JVM uses JBR 17.0.14.

Only KFlate was timed. Saved Kompress timings are accepted unchanged from `performance/history.json`. The Kompress comparison uses only Kompress-produced streams, matching the archived benchmark inputs.

Three one-second warmups, five one-second measurements, one fork per JVM case. These short development measurements expose broad tradeoffs; they are not release-grade speed claims. Raw samples and errors are included.

## jvm, rawDeflateDecompressionFromKFlate

| Corpus | Before ms | After ms | Before/after speedup | Saved Kompress/after speedup |
| --- | ---: | ---: | ---: | ---: |
| simpleText | 0.0029 | 0.0028 | 1.036x | Different input stream |
| text | 7.2320 | 6.0596 | 1.193x | Different input stream |
| model3D | 0.0203 | 0.0201 | 1.011x | Different input stream |
| Rainier.bmp | 37.2355 | 34.7857 | 1.070x | Different input stream |
| Maltese.bmp | 91.8651 | 82.6290 | 1.112x | Different input stream |
| Sunrise.bmp | 363.8703 | 370.5068 | 0.982x | Different input stream |
| compressed_MVT.pbf | 1.0398 | 0.9554 | 1.088x | Different input stream |

## jvm, rawDeflateDecompressionFromKompress

| Corpus | Before ms | After ms | Before/after speedup | Saved Kompress/after speedup |
| --- | ---: | ---: | ---: | ---: |
| simpleText | 0.0029 | 0.0028 | 1.036x | 0.292x |
| text | 7.1466 | 5.7811 | 1.236x | 0.879x |
| model3D | 0.0241 | 0.0222 | 1.082x | 0.387x |
| Rainier.bmp | 36.4853 | 31.8823 | 1.144x | 0.527x |
| Maltese.bmp | 97.7610 | 78.1425 | 1.251x | 0.738x |
| Sunrise.bmp | 375.8864 | 338.8647 | 1.109x | 0.597x |
| compressed_MVT.pbf | 1.0341 | 1.0448 | 0.990x | 0.640x |


## Validation and reproduction

All 180 Linux JVM tests passed, covering dictionaries, streaming, output limits and overlap semantics. All seven level 6 fixtures passed JDK and KFlate round trips. All 14 decoder JMH cases completed. Native and Wasm were not timed for this candidate.

Tracked source diff SHA-256: `7fd5f9a4495062d50ebb25ee8b5b276cab5859703da6e13a464685b697a6f5c1`. Added source/test files are present in this commit.

Linux build: `ANDROID_HOME=/home/rafael/Android/Sdk ./gradlew :kflate:jvmTest :kflate:jvmBenchmarkBenchmarkJar --no-parallel --max-workers=1`.

For level 9 timing, set `BENCHMARK_COMPRESSION_LEVEL` to 9 in `BenchmarkState.kt` before building; leave it at 6 otherwise. Use JBR 17.0.14 and the generated JMH jar with the main and benchmark classes on its classpath. Run `org.openjdk.jmh.Main` with the relevant `CompressionBenchmarks` method filter and `-wi 3 -i 5 -w 1s -r 1s -f 1 -foe true -rf json -rff result.json`. Only KFlate benchmark methods should be selected.

Compile `RatioSweep.java` against the KFlate classes and generated benchmark jar, then run it with `kflate/src/jvmTest/resources FIRST_LEVEL LAST_LEVEL`. It checks both JDK and KFlate decompression and records deterministic sizes and output hashes.

`./gradlew :kflate:koverVerifyJvm` passed on Linux with 100% instruction and branch coverage.
39 changes: 39 additions & 0 deletions performance/2026-09-09-copy/RatioSweep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import com.rafambn.kflate.KFlate;
import com.rafambn.kflate.compression.Raw;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.zip.Inflater;

public class RatioSweep {
public static void main(String[] args) throws Exception {
String[] names = {"simpleText", "text", "model3D", "Rainier.bmp", "Maltese.bmp", "Sunrise.bmp", "compressed_MVT.pbf"};
int first = args.length > 1 ? Integer.parseInt(args[1]) : 0;
int last = args.length > 2 ? Integer.parseInt(args[2]) : 9;
for (String name : names) {
byte[] input = Files.readAllBytes(Path.of(args[0], name));
for (int level = first; level <= last; level++) {
long start = System.nanoTime();
byte[] output = KFlate.INSTANCE.compress(input, new Raw(level, null));
double ms = (System.nanoTime() - start) / 1e6;
Inflater inflater = new Inflater(true);
try {
inflater.setInput(output);
byte[] decoded = new byte[input.length + 1];
int count = inflater.inflate(decoded);
if (!inflater.finished() || count != input.length || !Arrays.equals(input, Arrays.copyOf(decoded, count))) {
throw new AssertionError("Invalid output: " + name + " level " + level);
}
} finally {
inflater.end();
}
if (!Arrays.equals(input, KFlate.INSTANCE.decompress(output, new com.rafambn.kflate.decompression.Raw(null, null)))) {
throw new AssertionError("KFlate round trip: " + name + " level " + level);
}
System.out.printf(java.util.Locale.ROOT, "{\"corpus\":\"%s\",\"level\":%d,\"originalSizeBytes\":%d,\"compressedSizeBytes\":%d,\"singleRunMs\":%.6f,\"sha256\":\"%s\"}%n", name, level, input.length, output.length, ms, HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(output)));
}
}
}
}
Loading