Skip to content

Commit 46af178

Browse files
committed
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). Also add test128 to CI and check, wire species test tasks to the test classpath, and allow JMH runs to pass org.simdjson.species via Gradle. Signed-off-by: The-Alchemist <kap4020@gmail.com>
1 parent a4044aa commit 46af178

4 files changed

Lines changed: 217 additions & 14 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
strategy:
1010
matrix:
1111
version: [ 25 ]
12-
vector-length: [ 256, 512 ]
12+
vector-length: [ 128, 256, 512 ]
1313

1414
steps:
1515
- uses: actions/checkout@v4

build.gradle

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,19 @@ java {
7272
ext {
7373
junitVersion = '5.12.0'
7474
jsoniterScalaVersion = '2.33.2'
75+
scalaVersion = '2.13.18'
76+
}
77+
78+
configurations.named('zinc') {
79+
resolutionStrategy {
80+
force "org.scala-lang:scala-compiler:${scalaVersion}"
81+
force "org.scala-lang:scala-library:${scalaVersion}"
82+
force "org.scala-lang:scala-reflect:${scalaVersion}"
83+
}
7584
}
7685

7786
dependencies {
87+
jmhImplementation group: 'org.scala-lang', name: 'scala-library', version: scalaVersion
7888
jmhImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
7989
jmhImplementation group: 'com.alibaba.fastjson2', name: 'fastjson2', version: '2.0.56'
8090
jmhImplementation group: 'com.github.plokhotnyuk.jsoniter-scala', name: 'jsoniter-scala-core_2.13', version: jsoniterScalaVersion
@@ -147,15 +157,14 @@ test {
147157
failOnNoDiscoveredTests = false
148158
}
149159

150-
// Generate test tasks for specific species (256, 512)
151-
[256, 512].each { species ->
160+
// Generate test tasks for specific species (128, 256, 512)
161+
[128, 256, 512].each { species ->
152162
tasks.register("test${species}", Test) {
153163
group = 'verification'
154164
description = "Runs tests with org.simdjson.species=${species}"
155165

156-
// Fix: Removed 'dependsOn test'. This allows test256 to run independently.
157-
// We use mustRunAfter so they don't interleave output if run together via 'check'.
158-
dependsOn tasks.named('test')
166+
testClassesDirs = sourceSets.test.output.classesDirs
167+
classpath = sourceSets.test.runtimeClasspath
159168

160169
useJUnitPlatform()
161170
jvmArgs += [
@@ -167,6 +176,7 @@ test {
167176
}
168177

169178
tasks.named('check') {
179+
dependsOn tasks.named('test128')
170180
dependsOn tasks.named('test256')
171181
dependsOn tasks.named('test512')
172182
}
@@ -179,7 +189,17 @@ tasks.withType(JavaCompile).configureEach {
179189
options.compilerArgs.add("--add-modules=jdk.incubator.vector")
180190
}
181191

182-
tasks.compileJmhScala.classpath = sourceSets.main.compileClasspath
192+
tasks.named('compileJmhScala', ScalaCompile) {
193+
// jsoniter macros only need the JMH dependency classpath, not main (Java 25) outputs
194+
classpath = sourceSets.jmh.compileClasspath
195+
def jmhScalaJavaHome = System.getenv('JMH_SCALA_JAVA_HOME')
196+
if (jmhScalaJavaHome != null) {
197+
def forkJava = new File(jmhScalaJavaHome, 'bin/java')
198+
if (forkJava.exists()) {
199+
options.forkOptions.executable = forkJava.absolutePath
200+
}
201+
}
202+
}
183203
tasks.compileJmhJava.classpath += files(sourceSets.jmh.scala.classesDirectory)
184204

185205
compileTestJava {
@@ -194,11 +214,21 @@ javadoc.options {
194214

195215
jmh {
196216
fork = 1
197-
warmupIterations = 3
198-
iterations = 5
199-
jvmArgsPrepend = [
200-
'--add-modules=jdk.incubator.vector'
201-
]
217+
warmupIterations = project.hasProperty('jmh.warmupIterations')
218+
? project.property('jmh.warmupIterations').toString().toInteger() : 3
219+
iterations = project.hasProperty('jmh.iterations')
220+
? project.property('jmh.iterations').toString().toInteger() : 5
221+
if (project.hasProperty('jmh.warmup')) {
222+
warmup = project.property('jmh.warmup').toString()
223+
}
224+
if (project.hasProperty('jmh.timeOnIteration')) {
225+
timeOnIteration = project.property('jmh.timeOnIteration').toString()
226+
}
227+
def jmhJvmArgs = ['--add-modules=jdk.incubator.vector']
228+
if (project.hasProperty('jmh.species')) {
229+
jmhJvmArgs += "-Dorg.simdjson.species=${project.property('jmh.species')}"
230+
}
231+
jvmArgsPrepend = jmhJvmArgs
202232
if (OperatingSystem.current().isLinux()) {
203233
def profilerList = []
204234
if (getBooleanProperty('jmh.asyncProfilerEnabled', false)) {

src/main/java/org/simdjson/StructuralIndexer.java

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import java.util.Arrays;
77

8+
import static jdk.incubator.vector.ByteVector.SPECIES_128;
89
import static jdk.incubator.vector.ByteVector.SPECIES_256;
910
import static jdk.incubator.vector.ByteVector.SPECIES_512;
1011
import static jdk.incubator.vector.VectorOperators.ULE;
@@ -42,12 +43,179 @@ class StructuralIndexer {
4243
void index(byte[] buffer, int length) {
4344
bitIndexes.reset();
4445
switch (VECTOR_BIT_SIZE) {
46+
case 128 -> index128(buffer, length);
4547
case 256 -> index256(buffer, length);
4648
case 512 -> index512(buffer, length);
47-
default -> throw new UnsupportedOperationException("Unsupported vector width: " + VECTOR_BIT_SIZE * 64);
49+
default -> throw new UnsupportedOperationException("Unsupported vector width: " + VECTOR_BIT_SIZE);
4850
}
4951
}
5052

53+
/**
54+
* Stage-1 indexer for 128-bit vectors: four 16-byte lanes per 64-byte block (same layout as simdjson ARM64
55+
* {@code simd8x64} / {@code index<64>}).
56+
*/
57+
private void index128(byte[] buffer, int length) {
58+
long prevInString = 0;
59+
long prevEscaped = 0;
60+
long prevStructurals = 0;
61+
long unescapedCharsError = 0;
62+
long prevScalar = 0;
63+
64+
int loopBound = SPECIES_512.loopBound(length);
65+
int offset = 0;
66+
int blockIndex = 0;
67+
for (; offset < loopBound; offset += STEP_SIZE) {
68+
ByteVector chunk0 = ByteVector.fromArray(SPECIES_128, buffer, offset);
69+
ByteVector chunk1 = ByteVector.fromArray(SPECIES_128, buffer, offset + 16);
70+
ByteVector chunk2 = ByteVector.fromArray(SPECIES_128, buffer, offset + 32);
71+
ByteVector chunk3 = ByteVector.fromArray(SPECIES_128, buffer, offset + 48);
72+
73+
long backslash = pack128(
74+
chunk0.eq(BACKSLASH).toLong(),
75+
chunk1.eq(BACKSLASH).toLong(),
76+
chunk2.eq(BACKSLASH).toLong(),
77+
chunk3.eq(BACKSLASH).toLong());
78+
79+
long escaped;
80+
if (backslash == 0) {
81+
escaped = prevEscaped;
82+
prevEscaped = 0;
83+
} else {
84+
backslash &= ~prevEscaped;
85+
long followsEscape = backslash << 1 | prevEscaped;
86+
long oddSequenceStarts = backslash & ODD_BITS_MASK & ~followsEscape;
87+
88+
long sequencesStartingOnEvenBits = oddSequenceStarts + backslash;
89+
prevEscaped = ((oddSequenceStarts >>> 1) + (backslash >>> 1) + ((oddSequenceStarts & backslash) & 1)) >>> 63;
90+
91+
long invertMask = sequencesStartingOnEvenBits << 1;
92+
escaped = (EVEN_BITS_MASK ^ invertMask) & followsEscape;
93+
}
94+
95+
long unescaped = pack128(
96+
chunk0.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
97+
chunk1.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
98+
chunk2.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
99+
chunk3.compare(ULE, LAST_CONTROL_CHARACTER).toLong());
100+
101+
long quote0 = chunk0.eq(QUOTE).toLong();
102+
long quote1 = chunk1.eq(QUOTE).toLong();
103+
long quote2 = chunk2.eq(QUOTE).toLong();
104+
long quote3 = chunk3.eq(QUOTE).toLong();
105+
long quote = pack128(quote0, quote1, quote2, quote3) & ~escaped;
106+
107+
long inString = prefixXor(quote) ^ prevInString;
108+
prevInString = inString >> 63;
109+
110+
VectorShuffle<Byte> chunk0Low = chunk0.and(LOW_NIBBLE_MASK).toShuffle();
111+
VectorShuffle<Byte> chunk1Low = chunk1.and(LOW_NIBBLE_MASK).toShuffle();
112+
VectorShuffle<Byte> chunk2Low = chunk2.and(LOW_NIBBLE_MASK).toShuffle();
113+
VectorShuffle<Byte> chunk3Low = chunk3.and(LOW_NIBBLE_MASK).toShuffle();
114+
115+
long whitespace = pack128(
116+
chunk0.eq(WHITESPACE_TABLE.rearrange(chunk0Low)).toLong(),
117+
chunk1.eq(WHITESPACE_TABLE.rearrange(chunk1Low)).toLong(),
118+
chunk2.eq(WHITESPACE_TABLE.rearrange(chunk2Low)).toLong(),
119+
chunk3.eq(WHITESPACE_TABLE.rearrange(chunk3Low)).toLong());
120+
121+
long op = pack128(
122+
chunk0.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk0Low)).toLong(),
123+
chunk1.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk1Low)).toLong(),
124+
chunk2.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk2Low)).toLong(),
125+
chunk3.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk3Low)).toLong());
126+
127+
long scalar = ~(op | whitespace);
128+
long nonQuoteScalar = scalar & ~quote;
129+
long followsNonQuoteScalar = nonQuoteScalar << 1 | prevScalar;
130+
prevScalar = nonQuoteScalar >>> 63;
131+
long potentialScalarStart = scalar & ~followsNonQuoteScalar;
132+
long potentialStructuralStart = op | potentialScalarStart;
133+
bitIndexes.write(blockIndex, prevStructurals);
134+
blockIndex += STEP_SIZE;
135+
prevStructurals = potentialStructuralStart & ~(inString ^ quote);
136+
unescapedCharsError |= unescaped & inString;
137+
}
138+
139+
byte[] remainder = remainder(buffer, length, blockIndex);
140+
ByteVector chunk0 = ByteVector.fromArray(SPECIES_128, remainder, 0);
141+
ByteVector chunk1 = ByteVector.fromArray(SPECIES_128, remainder, 16);
142+
ByteVector chunk2 = ByteVector.fromArray(SPECIES_128, remainder, 32);
143+
ByteVector chunk3 = ByteVector.fromArray(SPECIES_128, remainder, 48);
144+
145+
long backslash = pack128(
146+
chunk0.eq(BACKSLASH).toLong(),
147+
chunk1.eq(BACKSLASH).toLong(),
148+
chunk2.eq(BACKSLASH).toLong(),
149+
chunk3.eq(BACKSLASH).toLong());
150+
151+
long escaped;
152+
if (backslash == 0) {
153+
escaped = prevEscaped;
154+
} else {
155+
backslash &= ~prevEscaped;
156+
long followsEscape = backslash << 1 | prevEscaped;
157+
long oddSequenceStarts = backslash & ODD_BITS_MASK & ~followsEscape;
158+
159+
long sequencesStartingOnEvenBits = oddSequenceStarts + backslash;
160+
long invertMask = sequencesStartingOnEvenBits << 1;
161+
escaped = (EVEN_BITS_MASK ^ invertMask) & followsEscape;
162+
}
163+
164+
long unescaped = pack128(
165+
chunk0.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
166+
chunk1.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
167+
chunk2.compare(ULE, LAST_CONTROL_CHARACTER).toLong(),
168+
chunk3.compare(ULE, LAST_CONTROL_CHARACTER).toLong());
169+
170+
long quote = pack128(
171+
chunk0.eq(QUOTE).toLong(),
172+
chunk1.eq(QUOTE).toLong(),
173+
chunk2.eq(QUOTE).toLong(),
174+
chunk3.eq(QUOTE).toLong()) & ~escaped;
175+
176+
long inString = prefixXor(quote) ^ prevInString;
177+
prevInString = inString >> 63;
178+
179+
VectorShuffle<Byte> chunk0Low = chunk0.and(LOW_NIBBLE_MASK).toShuffle();
180+
VectorShuffle<Byte> chunk1Low = chunk1.and(LOW_NIBBLE_MASK).toShuffle();
181+
VectorShuffle<Byte> chunk2Low = chunk2.and(LOW_NIBBLE_MASK).toShuffle();
182+
VectorShuffle<Byte> chunk3Low = chunk3.and(LOW_NIBBLE_MASK).toShuffle();
183+
184+
long whitespace = pack128(
185+
chunk0.eq(WHITESPACE_TABLE.rearrange(chunk0Low)).toLong(),
186+
chunk1.eq(WHITESPACE_TABLE.rearrange(chunk1Low)).toLong(),
187+
chunk2.eq(WHITESPACE_TABLE.rearrange(chunk2Low)).toLong(),
188+
chunk3.eq(WHITESPACE_TABLE.rearrange(chunk3Low)).toLong());
189+
190+
long op = pack128(
191+
chunk0.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk0Low)).toLong(),
192+
chunk1.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk1Low)).toLong(),
193+
chunk2.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk2Low)).toLong(),
194+
chunk3.or((byte) 0x20).eq(OP_TABLE.rearrange(chunk3Low)).toLong());
195+
196+
long scalar = ~(op | whitespace);
197+
long nonQuoteScalar = scalar & ~quote;
198+
long followsNonQuoteScalar = nonQuoteScalar << 1 | prevScalar;
199+
long potentialScalarStart = scalar & ~followsNonQuoteScalar;
200+
long potentialStructuralStart = op | potentialScalarStart;
201+
bitIndexes.write(blockIndex, prevStructurals);
202+
blockIndex += STEP_SIZE;
203+
prevStructurals = potentialStructuralStart & ~(inString ^ quote);
204+
unescapedCharsError |= unescaped & inString;
205+
bitIndexes.write(blockIndex, prevStructurals);
206+
bitIndexes.finish();
207+
if (prevInString != 0) {
208+
throw new JsonParsingException("Unclosed string. A string is opened, but never closed.");
209+
}
210+
if (unescapedCharsError != 0) {
211+
throw new JsonParsingException("Unescaped characters. Within strings, there are characters that should be escaped.");
212+
}
213+
}
214+
215+
private static long pack128(long mask0, long mask1, long mask2, long mask3) {
216+
return mask0 | (mask1 << 16) | (mask2 << 32) | (mask3 << 48);
217+
}
218+
51219
private void index256(byte[] buffer, int length) {
52220
long prevInString = 0;
53221
long prevEscaped = 0;

src/main/java/org/simdjson/VectorUtils.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,17 @@ class VectorUtils {
2727
BYTE_SPECIES = ByteVector.SPECIES_256;
2828
INT_SPECIES = IntVector.SPECIES_256;
2929
}
30+
case "128" -> {
31+
BYTE_SPECIES = ByteVector.SPECIES_128;
32+
INT_SPECIES = IntVector.SPECIES_128;
33+
}
3034
default -> throw new IllegalArgumentException("Unsupported vector species: " + species);
3135
}
3236
}
3337

3438
private static void assertSupportForSpecies(VectorSpecies<?> species) {
35-
if (species.vectorShape() != VectorShape.S_256_BIT && species.vectorShape() != VectorShape.S_512_BIT) {
39+
VectorShape shape = species.vectorShape();
40+
if (shape != VectorShape.S_128_BIT && shape != VectorShape.S_256_BIT && shape != VectorShape.S_512_BIT) {
3641
throw new IllegalArgumentException("Unsupported vector species: " + species);
3742
}
3843
}

0 commit comments

Comments
 (0)