diff --git a/build.gradle b/build.gradle index 3609523..06759b3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,4 @@ -import me.champeau.jmh.JmhBytecodeGeneratorTask import org.gradle.internal.os.OperatingSystem - import java.time.Duration plugins { @@ -81,6 +79,9 @@ dependencies { jmhImplementation group: 'com.google.guava', name: 'guava', version: '33.4.0-jre' compileOnly group: 'com.github.plokhotnyuk.jsoniter-scala', name: 'jsoniter-scala-macros_2.13', version: jsoniterScalaVersion + jmhImplementation group: 'org.openjdk.jmh', name: 'jmh-core', version: '1.37' + jmhImplementation group: 'org.openjdk.jmh', name: 'jmh-generator-annprocess', version: '1.37' + jmhAnnotationProcessor group: 'org.openjdk.jmh', name: 'jmh-generator-annprocess', version: '1.37' testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.27.3' testImplementation group: 'org.apache.commons', name: 'commons-text', version: '1.13.0' testImplementation group: 'org.junit-pioneer', name: 'junit-pioneer', version: '2.3.0' @@ -123,7 +124,7 @@ tasks.register('downloadTestData', Exec) { } } -// Configuration common to ALL Test tasks (including 'test', 'test256', 'test512') +// Configuration common to ALL Test tasks tasks.withType(Test).configureEach { dependsOn tasks.named('downloadTestData') @@ -171,7 +172,8 @@ tasks.named('check') { dependsOn tasks.named('test512') } -tasks.withType(JmhBytecodeGeneratorTask).configureEach { +// Fix: Use Fully Qualified Name here instead of importing it at the top +tasks.withType(me.champeau.jmh.JmhBytecodeGeneratorTask).configureEach { jvmArgs.set(["--add-modules=jdk.incubator.vector"]) } diff --git a/src/jmh/java/org/simdjson/ParseAndSelectBenchmark.java b/src/jmh/java/org/simdjson/ParseAndSelectBenchmark.java index 1cae0b1..71f5aa1 100644 --- a/src/jmh/java/org/simdjson/ParseAndSelectBenchmark.java +++ b/src/jmh/java/org/simdjson/ParseAndSelectBenchmark.java @@ -6,15 +6,24 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.profile.GCProfiler; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -27,8 +36,18 @@ @OutputTimeUnit(TimeUnit.SECONDS) public class ParseAndSelectBenchmark { + private static final String STATUSES = "statuses"; + private static final byte[] STATUSES_BYTES = STATUSES.getBytes(StandardCharsets.UTF_8); + private static final String USER = "user"; + private static final byte[] USER_BYTES = USER.getBytes(StandardCharsets.UTF_8); + private static final String DEFAULT_PROFILE = "default_profile"; + private static final byte[] DEFAULT_PROFILE_BYTES = DEFAULT_PROFILE.getBytes(StandardCharsets.UTF_8); + private static final String SCREEN_NAME = "screen_name"; + private static final byte[] SCREEN_NAME_BYTES = SCREEN_NAME.getBytes(StandardCharsets.UTF_8); private final SimdJsonParser simdJsonParser = new SimdJsonParser(); private final ObjectMapper objectMapper = new ObjectMapper(); + private final Set defaultUsers = new HashSet<>(); + private final CapturedJsonValueConsumer capturedNameConsumer = new CapturedJsonValueConsumer(); private byte[] buffer; private byte[] bufferPadded; @@ -42,63 +61,263 @@ public void setup() throws IOException { System.out.println("VectorSpecies = " + VectorUtils.BYTE_SPECIES); } + @TearDown(Level.Trial) + public void tearDownPerTrial() { + System.out.println("defaultUsers.size() = " + defaultUsers.size()); + defaultUsers.clear(); + capturedNameConsumer.reset(); + } + @Benchmark + @Fork(jvmArgsAppend = {"-Xmx512m", "-Xms512m", "-XX:MaxDirectMemorySize=1g"}) public int countUniqueUsersWithDefaultProfile_jackson() throws IOException { JsonNode jacksonJsonNode = objectMapper.readTree(buffer); - Set defaultUsers = new HashSet<>(); - Iterator tweets = jacksonJsonNode.get("statuses").elements(); + Iterator tweets = jacksonJsonNode.get(STATUSES).elements(); while (tweets.hasNext()) { JsonNode tweet = tweets.next(); - JsonNode user = tweet.get("user"); - if (user.get("default_profile").asBoolean()) { - defaultUsers.add(user.get("screen_name").textValue()); + JsonNode user = tweet.get(USER); + if (user.get(DEFAULT_PROFILE).asBoolean()) { + defaultUsers.add(user.get(SCREEN_NAME).textValue()); } } return defaultUsers.size(); } @Benchmark + @Fork(jvmArgsAppend = {"-Xmx512m", "-Xms512m", "-XX:MaxDirectMemorySize=1g"}) public int countUniqueUsersWithDefaultProfile_fastjson() { JSONObject jsonObject = (JSONObject) JSON.parse(buffer); - Set defaultUsers = new HashSet<>(); - Iterator tweets = jsonObject.getJSONArray("statuses").iterator(); + Iterator tweets = jsonObject.getJSONArray(STATUSES).iterator(); while (tweets.hasNext()) { JSONObject tweet = (JSONObject) tweets.next(); - JSONObject user = (JSONObject) tweet.get("user"); - if (user.getBoolean("default_profile")) { - defaultUsers.add(user.getString("screen_name")); + JSONObject user = (JSONObject) tweet.get(USER); + if (user.getBoolean(DEFAULT_PROFILE)) { + defaultUsers.add(user.getString(SCREEN_NAME)); } } return defaultUsers.size(); } @Benchmark + @Fork(jvmArgsAppend = {"-Xmx512m", "-Xms512m", "-XX:MaxDirectMemorySize=1g"}) public int countUniqueUsersWithDefaultProfile_simdjson() { JsonValue simdJsonValue = simdJsonParser.parse(buffer, buffer.length); - Set defaultUsers = new HashSet<>(); - Iterator tweets = simdJsonValue.get("statuses").arrayIterator(); + Iterator tweets = simdJsonValue.get(STATUSES).arrayIterator(); while (tweets.hasNext()) { JsonValue tweet = tweets.next(); - JsonValue user = tweet.get("user"); - if (user.get("default_profile").asBoolean()) { - defaultUsers.add(user.get("screen_name").asString()); + JsonValue user = tweet.get(USER); + if (user.get(DEFAULT_PROFILE).asBoolean()) { + defaultUsers.add(user.get(SCREEN_NAME).asString()); } } return defaultUsers.size(); } @Benchmark + @Fork(jvmArgsAppend = {"-Xmx512m", "-Xms512m", "-XX:MaxDirectMemorySize=1g"}) + public int countUniqueUsersWithDefaultProfile_simdjson_gc_free() { + JsonValue simdJsonValue = simdJsonParser.parse(buffer, buffer.length); + Iterator tweets = simdJsonValue.get(STATUSES_BYTES).arrayIterator(); + while (tweets.hasNext()) { + JsonValue tweet = tweets.next(); + JsonValue user = tweet.get(USER_BYTES); + if (user.get(DEFAULT_PROFILE_BYTES).asBoolean()) { + JsonValue jsonValue = user.get(SCREEN_NAME_BYTES); + jsonValue.acceptBytes(capturedNameConsumer); + String capturedName = capturedNameConsumer.getCapturedName(); + if (capturedName != null) { + defaultUsers.add(capturedName); + } + } + } + return defaultUsers.size(); + } + + @Benchmark + @Fork(jvmArgsAppend = {"-Xmx512m", "-Xms512m", "-XX:MaxDirectMemorySize=1g"}) public int countUniqueUsersWithDefaultProfile_simdjsonPadded() { JsonValue simdJsonValue = simdJsonParser.parse(bufferPadded, buffer.length); - Set defaultUsers = new HashSet<>(); - Iterator tweets = simdJsonValue.get("statuses").arrayIterator(); + Iterator tweets = simdJsonValue.get(STATUSES).arrayIterator(); while (tweets.hasNext()) { JsonValue tweet = tweets.next(); - JsonValue user = tweet.get("user"); - if (user.get("default_profile").asBoolean()) { - defaultUsers.add(user.get("screen_name").asString()); + JsonValue user = tweet.get(USER); + if (user.get(DEFAULT_PROFILE).asBoolean()) { + defaultUsers.add(user.get(SCREEN_NAME).asString()); } } return defaultUsers.size(); } + + @Benchmark + @Fork(jvmArgsAppend = {"-Xmx512m", "-Xms512m", "-XX:MaxDirectMemorySize=1g"}) + public int countUniqueUsersWithDefaultProfile_simdjsonPadded_gc_free() { + JsonValue simdJsonValue = simdJsonParser.parse(bufferPadded, buffer.length); + Iterator tweets = simdJsonValue.get(STATUSES_BYTES).arrayIterator(); + while (tweets.hasNext()) { + JsonValue tweet = tweets.next(); + JsonValue user = tweet.get(USER_BYTES); + if (user.get(DEFAULT_PROFILE_BYTES).asBoolean()) { + JsonValue jsonValue = user.get(SCREEN_NAME_BYTES); + jsonValue.acceptBytes(capturedNameConsumer); + String capturedName = capturedNameConsumer.getCapturedName(); + if (capturedName != null) { + defaultUsers.add(capturedName); + } + } + } + return defaultUsers.size(); + } + + private static class CapturedJsonValueConsumer implements JsonValue.JsonValueByteConsumer { + private final HashMap byteSequenceHashMap = new HashMap<>(); + // reused lookup key to avoid allocation on hits + private final BytesKey lookupKey = new BytesKey(); + private String capturedName; + + @Override + public void acceptBytes(byte[] bytes, int offset, int length) { + this.capturedName = null; + // 1) lookup + lookupKey.wrap(bytes, offset, length); + String cached = byteSequenceHashMap.get(lookupKey); + if (cached != null) { + capturedName = cached; + return; + } + String value = new String(bytes, offset, length, StandardCharsets.UTF_8); + byteSequenceHashMap + .put(new BytesKey(bytes, offset, length), value); + this.capturedName = value; + } + + public String getCapturedName() { + return capturedName; + } + + public void reset() { + this.capturedName = null; + this.byteSequenceHashMap.clear(); + } + } + + /** + * Immutable-ish bytes-slice key with content-based hash/equals. + * IMPORTANT: only safe if the underlying bytes do not change for the lifetime of the map entries. + * (In your simdjson case: stringBuffer is stable, so this is fine.) + */ + private static final class BytesKey { + private byte[] bytes; + private int offset; + private int length; + private int hash; // cached + + BytesKey() { + } + + BytesKey(byte[] bytes, int offset, int length) { + wrap(bytes, offset, length); + } + + void wrap(byte[] bytes, int offset, int length) { + this.bytes = bytes; + this.offset = offset; + this.length = length; + this.hash = fnv1a32(bytes, offset, length); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BytesKey other)) return false; + if (this.length != other.length) return false; + return bytesEqual(this.bytes, this.offset, other.bytes, other.offset, this.length); + } + + private static boolean bytesEqual(byte[] a, int aOff, byte[] b, int bOff, int len) { + for (int i = 0; i < len; i++) { + if (a[aOff + i] != b[bOff + i]) return false; + } + return true; + } + + // fast stable hash for byte slices + private static int fnv1a32(byte[] a, int off, int len) { + int h = 0x811C9DC5; + int end = off + len; + for (int i = off; i < end; i++) { + h ^= (a[i] & 0xFF); + h *= 0x01000193; + } + return h; + } + } + + //Benchmark Mode Cnt Score Error Units + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson thrpt 2 424.752 ops/s + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson:gc.alloc.rate thrpt 2 739.232 MB/sec + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson:gc.alloc.rate.norm thrpt 2 1825142.125 B/op + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson:gc.count thrpt 2 4244.000 counts + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson:gc.time thrpt 2 4788.000 ms + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson thrpt 2 409.952 ops/s + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson:gc.alloc.rate thrpt 2 570.240 MB/sec + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson:gc.alloc.rate.norm thrpt 2 1458734.186 B/op + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson:gc.count thrpt 2 2651.000 counts + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson:gc.time thrpt 2 3014.000 ms + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson thrpt 2 1526.368 ops/s + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson:gc.alloc.rate thrpt 2 19.179 MB/sec + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson:gc.alloc.rate.norm thrpt 2 13177.708 B/op + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson:gc.count thrpt 2 232.000 counts + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson:gc.time thrpt 2 327.000 ms + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded thrpt 2 1472.190 ops/s + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded:gc.alloc.rate thrpt 2 18.500 MB/sec + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded:gc.alloc.rate.norm thrpt 2 13177.770 B/op + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded:gc.count thrpt 2 253.000 counts + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded:gc.time thrpt 2 371.000 ms + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded_gc_free thrpt 2 1529.952 ops/s + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded_gc_free:gc.alloc.rate thrpt 2 0.049 MB/sec + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded_gc_free:gc.alloc.rate.norm thrpt 2 33.686 B/op + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded_gc_free:gc.count thrpt 2 2.000 counts + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded_gc_free:gc.time thrpt 2 8.000 ms + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson_gc_free thrpt 2 1524.408 ops/s + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson_gc_free:gc.alloc.rate thrpt 2 0.049 MB/sec + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson_gc_free:gc.alloc.rate.norm thrpt 2 33.607 B/op + //ParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson_gc_free:gc.count thrpt 2 ≈ 0 counts + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson thrpt 2 1169.618 ops/s + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson:gc.alloc.rate thrpt 2 227.498 MB/sec + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson:gc.alloc.rate.norm thrpt 2 203960.598 B/op + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson:gc.count thrpt 2 23.000 counts + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_fastjson:gc.time thrpt 2 20.000 ms + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson thrpt 2 776.049 ops/s + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson:gc.alloc.rate thrpt 2 332.906 MB/sec + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson:gc.alloc.rate.norm thrpt 2 449824.902 B/op + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson:gc.count thrpt 2 11.000 counts + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_jackson:gc.time thrpt 2 10.000 ms + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson thrpt 2 2536.616 ops/s + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson:gc.alloc.rate thrpt 2 59.315 MB/sec + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson:gc.alloc.rate.norm thrpt 2 24520.276 B/op + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson:gc.count thrpt 2 4.000 counts + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjson:gc.time thrpt 2 6.000 ms + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded thrpt 2 2748.359 ops/s + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded:gc.alloc.rate thrpt 2 64.267 MB/sec + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded:gc.alloc.rate.norm thrpt 2 24520.257 B/op + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded:gc.count thrpt 2 7.000 counts + //SchemaBasedParseAndSelectBenchmark.countUniqueUsersWithDefaultProfile_simdjsonPadded:gc.time thrpt 2 9.000 ms + + /** + * to run this from ide, i.e intellij please add "--add-modules jdk.incubator.vector" in VM options. + */ + static void main(String[] args) throws RunnerException { + Options options = new OptionsBuilder().include(ParseAndSelectBenchmark.class.getSimpleName()). + forks(1). + warmupIterations(2). + measurementIterations(2) + .addProfiler(GCProfiler.class) + .build(); + new Runner(options).run(); + } } diff --git a/src/main/java/org/simdjson/JsonValue.java b/src/main/java/org/simdjson/JsonValue.java index 6877519..79ca58c 100644 --- a/src/main/java/org/simdjson/JsonValue.java +++ b/src/main/java/org/simdjson/JsonValue.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.NoSuchElementException; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.simdjson.Tape.DOUBLE; import static org.simdjson.Tape.FALSE_VALUE; import static org.simdjson.Tape.INT64; @@ -13,7 +14,6 @@ import static org.simdjson.Tape.START_OBJECT; import static org.simdjson.Tape.STRING; import static org.simdjson.Tape.TRUE_VALUE; -import static java.nio.charset.StandardCharsets.UTF_8; public class JsonValue { @@ -82,6 +82,20 @@ public String asString() { return getString(tapeIdx); } + /** + * Reads a segment of bytes described by the internal state of the current object + * and passes it to the provided {@link JsonValueByteConsumer} for processing. + * + * @param consumer the {@link JsonValueByteConsumer} that processes the extracted bytes; + * it will receive a segment of the internal {@code stringBuffer} array + * defined by the current tape index and length + */ + public void acceptBytes(JsonValueByteConsumer consumer) { + int stringBufferIdx = (int) tape.getValue(tapeIdx); + int len = IntegerUtils.toInt(stringBuffer, stringBufferIdx); + consumer.acceptBytes(stringBuffer, stringBufferIdx + Integer.BYTES, len); + } + private String getString(int tapeIdx) { int stringBufferIdx = (int) tape.getValue(tapeIdx); int len = IntegerUtils.toInt(stringBuffer, stringBufferIdx); @@ -90,6 +104,10 @@ private String getString(int tapeIdx) { public JsonValue get(String name) { byte[] bytes = name.getBytes(UTF_8); + return get(bytes); + } + + public JsonValue get(byte[] name) { int idx = tapeIdx + 1; int endIdx = tape.getMatchingBraceIndex(tapeIdx) - 1; while (idx < endIdx) { @@ -99,7 +117,7 @@ public JsonValue get(String name) { idx = tape.computeNextIndex(valIdx); int stringBufferFromIdx = stringBufferIdx + Integer.BYTES; int stringBufferToIdx = stringBufferFromIdx + len; - if (Arrays.compare(bytes, 0, bytes.length, stringBuffer, stringBufferFromIdx, stringBufferToIdx) == 0) { + if (Arrays.compare(name, 0, name.length, stringBuffer, stringBufferFromIdx, stringBufferToIdx) == 0) { return new JsonValue(tape, valIdx, stringBuffer, buffer); } } @@ -218,4 +236,16 @@ public JsonValue setValue(JsonValue value) { throw new UnsupportedOperationException("Object fields are immutable"); } } + + public interface JsonValueByteConsumer { + + /** + * Consumes a segment of bytes from the provided array for processing. + * + * @param bytes the array of bytes to be processed + * @param offset the starting position in the array from which bytes should be read + * @param length the number of bytes to be read from the array, starting at the offset + */ + void acceptBytes(byte[] bytes, int offset, int length); + } }