From afee2c32e1b8070a20214e8728079d6759663c8f Mon Sep 17 00:00:00 2001 From: The-Alchemist Date: Thu, 17 Sep 2026 17:51:46 -0400 Subject: [PATCH] Add 128-bit vector support for stage-1 structural indexing Platforms such as Apple Silicon use 128-bit ByteVector as the preferred species; rejecting that width broke ./gradlew test and made forced 256-bit runs very slow. Implement index128 with four 16-byte lanes per 64-byte block, aligned with simdjson ARM64 stage-1 (simd8x64). Add test128 to CI and check, and wire species-specific test tasks to the test classpath so test128/test256/test512 run the suite with the correct JVM flag. Signed-off-by: The-Alchemist --- .github/workflows/ci.yml | 2 +- build.gradle | 14 +- .../java/org/simdjson/StructuralIndexer.java | 170 +++++++++++++++++- src/main/java/org/simdjson/VectorUtils.java | 7 +- 4 files changed, 183 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40e6796..bae1e30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: version: [ 25 ] - vector-length: [ 256, 512 ] + vector-length: [ 128, 256, 512 ] steps: - uses: actions/checkout@v4 diff --git a/build.gradle b/build.gradle index 3609523..d79e78f 100644 --- a/build.gradle +++ b/build.gradle @@ -123,7 +123,7 @@ tasks.register('downloadTestData', Exec) { } } -// Configuration common to ALL Test tasks (including 'test', 'test256', 'test512') +// Configuration common to ALL Test tasks (including 'test', 'test128', 'test256', 'test512') tasks.withType(Test).configureEach { dependsOn tasks.named('downloadTestData') @@ -138,7 +138,7 @@ tasks.withType(Test).configureEach { } } -// --- Test Variants (256/512) --- +// --- Test Variants (128/256/512) --- test { useJUnitPlatform() jvmArgs += [ @@ -147,15 +147,14 @@ test { failOnNoDiscoveredTests = false } -// Generate test tasks for specific species (256, 512) -[256, 512].each { species -> +// Generate test tasks for specific species (128, 256, 512) +[128, 256, 512].each { species -> tasks.register("test${species}", Test) { group = 'verification' description = "Runs tests with org.simdjson.species=${species}" - // Fix: Removed 'dependsOn test'. This allows test256 to run independently. - // We use mustRunAfter so they don't interleave output if run together via 'check'. - dependsOn tasks.named('test') + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath useJUnitPlatform() jvmArgs += [ @@ -167,6 +166,7 @@ test { } tasks.named('check') { + dependsOn tasks.named('test128') dependsOn tasks.named('test256') dependsOn tasks.named('test512') } diff --git a/src/main/java/org/simdjson/StructuralIndexer.java b/src/main/java/org/simdjson/StructuralIndexer.java index 1850742..f4409e0 100644 --- a/src/main/java/org/simdjson/StructuralIndexer.java +++ b/src/main/java/org/simdjson/StructuralIndexer.java @@ -5,6 +5,7 @@ import java.util.Arrays; +import static jdk.incubator.vector.ByteVector.SPECIES_128; import static jdk.incubator.vector.ByteVector.SPECIES_256; import static jdk.incubator.vector.ByteVector.SPECIES_512; import static jdk.incubator.vector.VectorOperators.ULE; @@ -42,12 +43,179 @@ class StructuralIndexer { void index(byte[] buffer, int length) { bitIndexes.reset(); switch (VECTOR_BIT_SIZE) { + case 128 -> index128(buffer, length); case 256 -> index256(buffer, length); case 512 -> index512(buffer, length); - default -> throw new UnsupportedOperationException("Unsupported vector width: " + VECTOR_BIT_SIZE * 64); + default -> throw new UnsupportedOperationException("Unsupported vector width: " + VECTOR_BIT_SIZE); } } + /** + * Stage-1 indexer for 128-bit vectors: four 16-byte lanes per 64-byte block (same layout as simdjson ARM64 + * {@code simd8x64} / {@code index<64>}). + */ + private void index128(byte[] buffer, int length) { + long prevInString = 0; + long prevEscaped = 0; + long prevStructurals = 0; + long unescapedCharsError = 0; + long prevScalar = 0; + + int loopBound = SPECIES_512.loopBound(length); + int offset = 0; + int blockIndex = 0; + for (; offset < loopBound; offset += STEP_SIZE) { + ByteVector chunk0 = ByteVector.fromArray(SPECIES_128, buffer, offset); + ByteVector chunk1 = ByteVector.fromArray(SPECIES_128, buffer, offset + 16); + ByteVector chunk2 = ByteVector.fromArray(SPECIES_128, buffer, offset + 32); + ByteVector chunk3 = ByteVector.fromArray(SPECIES_128, buffer, offset + 48); + + long backslash = pack128( + chunk0.eq(BACKSLASH).toLong(), + chunk1.eq(BACKSLASH).toLong(), + chunk2.eq(BACKSLASH).toLong(), + chunk3.eq(BACKSLASH).toLong()); + + long escaped; + if (backslash == 0) { + escaped = prevEscaped; + prevEscaped = 0; + } else { + backslash &= ~prevEscaped; + long followsEscape = backslash << 1 | prevEscaped; + long oddSequenceStarts = backslash & ODD_BITS_MASK & ~followsEscape; + + long sequencesStartingOnEvenBits = oddSequenceStarts + backslash; + prevEscaped = ((oddSequenceStarts >>> 1) + (backslash >>> 1) + ((oddSequenceStarts & backslash) & 1)) >>> 63; + + long invertMask = sequencesStartingOnEvenBits << 1; + escaped = (EVEN_BITS_MASK ^ invertMask) & followsEscape; + } + + long unescaped = pack128( + chunk0.compare(ULE, LAST_CONTROL_CHARACTER).toLong(), + chunk1.compare(ULE, LAST_CONTROL_CHARACTER).toLong(), + chunk2.compare(ULE, LAST_CONTROL_CHARACTER).toLong(), + chunk3.compare(ULE, LAST_CONTROL_CHARACTER).toLong()); + + long quote0 = chunk0.eq(QUOTE).toLong(); + long quote1 = chunk1.eq(QUOTE).toLong(); + long quote2 = chunk2.eq(QUOTE).toLong(); + long quote3 = chunk3.eq(QUOTE).toLong(); + long quote = pack128(quote0, quote1, quote2, quote3) & ~escaped; + + long inString = prefixXor(quote) ^ prevInString; + prevInString = inString >> 63; + + VectorShuffle chunk0Low = chunk0.and(LOW_NIBBLE_MASK).toShuffle(); + VectorShuffle chunk1Low = chunk1.and(LOW_NIBBLE_MASK).toShuffle(); + VectorShuffle chunk2Low = chunk2.and(LOW_NIBBLE_MASK).toShuffle(); + VectorShuffle chunk3Low = chunk3.and(LOW_NIBBLE_MASK).toShuffle(); + + long whitespace = pack128( + chunk0.eq(WHITESPACE_TABLE.rearrange(chunk0Low)).toLong(), + chunk1.eq(WHITESPACE_TABLE.rearrange(chunk1Low)).toLong(), + chunk2.eq(WHITESPACE_TABLE.rearrange(chunk2Low)).toLong(), + chunk3.eq(WHITESPACE_TABLE.rearrange(chunk3Low)).toLong()); + + long op = pack128( + chunk0.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk0Low)).toLong(), + chunk1.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk1Low)).toLong(), + chunk2.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk2Low)).toLong(), + chunk3.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk3Low)).toLong()); + + long scalar = ~(op | whitespace); + long nonQuoteScalar = scalar & ~quote; + long followsNonQuoteScalar = nonQuoteScalar << 1 | prevScalar; + prevScalar = nonQuoteScalar >>> 63; + long potentialScalarStart = scalar & ~followsNonQuoteScalar; + long potentialStructuralStart = op | potentialScalarStart; + bitIndexes.write(blockIndex, prevStructurals); + blockIndex += STEP_SIZE; + prevStructurals = potentialStructuralStart & ~(inString ^ quote); + unescapedCharsError |= unescaped & inString; + } + + byte[] remainder = remainder(buffer, length, blockIndex); + ByteVector chunk0 = ByteVector.fromArray(SPECIES_128, remainder, 0); + ByteVector chunk1 = ByteVector.fromArray(SPECIES_128, remainder, 16); + ByteVector chunk2 = ByteVector.fromArray(SPECIES_128, remainder, 32); + ByteVector chunk3 = ByteVector.fromArray(SPECIES_128, remainder, 48); + + long backslash = pack128( + chunk0.eq(BACKSLASH).toLong(), + chunk1.eq(BACKSLASH).toLong(), + chunk2.eq(BACKSLASH).toLong(), + chunk3.eq(BACKSLASH).toLong()); + + long escaped; + if (backslash == 0) { + escaped = prevEscaped; + } else { + backslash &= ~prevEscaped; + long followsEscape = backslash << 1 | prevEscaped; + long oddSequenceStarts = backslash & ODD_BITS_MASK & ~followsEscape; + + long sequencesStartingOnEvenBits = oddSequenceStarts + backslash; + long invertMask = sequencesStartingOnEvenBits << 1; + escaped = (EVEN_BITS_MASK ^ invertMask) & followsEscape; + } + + long unescaped = pack128( + chunk0.compare(ULE, LAST_CONTROL_CHARACTER).toLong(), + chunk1.compare(ULE, LAST_CONTROL_CHARACTER).toLong(), + chunk2.compare(ULE, LAST_CONTROL_CHARACTER).toLong(), + chunk3.compare(ULE, LAST_CONTROL_CHARACTER).toLong()); + + long quote = pack128( + chunk0.eq(QUOTE).toLong(), + chunk1.eq(QUOTE).toLong(), + chunk2.eq(QUOTE).toLong(), + chunk3.eq(QUOTE).toLong()) & ~escaped; + + long inString = prefixXor(quote) ^ prevInString; + prevInString = inString >> 63; + + VectorShuffle chunk0Low = chunk0.and(LOW_NIBBLE_MASK).toShuffle(); + VectorShuffle chunk1Low = chunk1.and(LOW_NIBBLE_MASK).toShuffle(); + VectorShuffle chunk2Low = chunk2.and(LOW_NIBBLE_MASK).toShuffle(); + VectorShuffle chunk3Low = chunk3.and(LOW_NIBBLE_MASK).toShuffle(); + + long whitespace = pack128( + chunk0.eq(WHITESPACE_TABLE.rearrange(chunk0Low)).toLong(), + chunk1.eq(WHITESPACE_TABLE.rearrange(chunk1Low)).toLong(), + chunk2.eq(WHITESPACE_TABLE.rearrange(chunk2Low)).toLong(), + chunk3.eq(WHITESPACE_TABLE.rearrange(chunk3Low)).toLong()); + + long op = pack128( + chunk0.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk0Low)).toLong(), + chunk1.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk1Low)).toLong(), + chunk2.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk2Low)).toLong(), + chunk3.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk3Low)).toLong()); + + long scalar = ~(op | whitespace); + long nonQuoteScalar = scalar & ~quote; + long followsNonQuoteScalar = nonQuoteScalar << 1 | prevScalar; + long potentialScalarStart = scalar & ~followsNonQuoteScalar; + long potentialStructuralStart = op | potentialScalarStart; + bitIndexes.write(blockIndex, prevStructurals); + blockIndex += STEP_SIZE; + prevStructurals = potentialStructuralStart & ~(inString ^ quote); + unescapedCharsError |= unescaped & inString; + bitIndexes.write(blockIndex, prevStructurals); + bitIndexes.finish(); + if (prevInString != 0) { + throw new JsonParsingException("Unclosed string. A string is opened, but never closed."); + } + if (unescapedCharsError != 0) { + throw new JsonParsingException("Unescaped characters. Within strings, there are characters that should be escaped."); + } + } + + private static long pack128(long mask0, long mask1, long mask2, long mask3) { + return mask0 | (mask1 << 16) | (mask2 << 32) | (mask3 << 48); + } + private void index256(byte[] buffer, int length) { long prevInString = 0; long prevEscaped = 0; diff --git a/src/main/java/org/simdjson/VectorUtils.java b/src/main/java/org/simdjson/VectorUtils.java index 7a1ce8f..8bc2082 100644 --- a/src/main/java/org/simdjson/VectorUtils.java +++ b/src/main/java/org/simdjson/VectorUtils.java @@ -27,12 +27,17 @@ class VectorUtils { BYTE_SPECIES = ByteVector.SPECIES_256; INT_SPECIES = IntVector.SPECIES_256; } + case "128" -> { + BYTE_SPECIES = ByteVector.SPECIES_128; + INT_SPECIES = IntVector.SPECIES_128; + } default -> throw new IllegalArgumentException("Unsupported vector species: " + species); } } private static void assertSupportForSpecies(VectorSpecies species) { - if (species.vectorShape() != VectorShape.S_256_BIT && species.vectorShape() != VectorShape.S_512_BIT) { + VectorShape shape = species.vectorShape(); + if (shape != VectorShape.S_128_BIT && shape != VectorShape.S_256_BIT && shape != VectorShape.S_512_BIT) { throw new IllegalArgumentException("Unsupported vector species: " + species); } }