Quarkus has a new JSON parser and object mapper called QSON. It does bytecode generation for the Java classes you want to map to and from JSON around a small core library. I’m not going to get into details on how to use it, just visit the github page for more information.
I started this project because I noticed a huge startup time for Jackson as relative to the other components within Quarkus applications. IIRC it was taking about 20% of the boot time for a simple JAX-RS microservice. So the initial prototype was to see how much I could improve boot time and I was pleasantly surprised that the parser I implemented was a bit better than Jackson at runtime too!
The end result was that boot time improved about 20% for a simple Quarkus JAX-RS microservice. The runtime performance is also better in most instances too. Here are the numbers from a JMH benchmark I did:
Benchmark Mode Cnt Score Error Units
MyBenchmark.testParserAfterburner thrpt 2 223630.276 ops/s
MyBenchmark.testParserJackson thrpt 2 218748.065 ops/s
MyBenchmark.testParserQson thrpt 2 251086.874 ops/s
MyBenchmark.testWriterAfterburner thrpt 2 189243.175 ops/s
MyBenchmark.testWriterJackson thrpt 2 168637.541 ops/s
MyBenchmark.testWriterQson thrpt 2 177855.879 ops/s
These are runtime throughput numbers so the higher the better. Qson is better than regular Jackson and Jackson+Afterburner for json to object mapping (reading/parsing). For output, Qson is better than regular Jackson, but is a little behind Afterburner.
There’s still some work to do for Qson. One of the big things I need is a maven and gradle plugin to handle bytecode generation so that Qson can be used outside of Quarkus. We’ll also be adding more features to Qson like custom mappings. One thing to note though is that I won’t add features that hurt performance, increase memory footprint, or hurt boot time.
Over time, we’ll be integrating Qson as an option for any Quarkus extension that needs Json object mapping. So far, I’ve done integration with JAX-RS (Resteasy). Funqy is a prime candidate next.
Dec 15, 2020 @ 17:47:28
Without any changes to the benchmark, this is what I got on my computer:
“`
Benchmark Mode Cnt Score Error Units
MyBenchmark.testParserAfterburner thrpt 2 195414,167 ops/s
MyBenchmark.testParserJackson thrpt 2 107052,709 ops/s
MyBenchmark.testParserQson thrpt 2 202947,336 ops/s
MyBenchmark.testWriterAfterburner thrpt 2 155204,306 ops/s
MyBenchmark.testWriterJackson thrpt 2 140841,650 ops/s
MyBenchmark.testWriterQson thrpt 2 56323,111 ops/s
“`
I’m running this on:
“`
> java –version
openjdk 15 2020-09-15
OpenJDK Runtime Environment AdoptOpenJDK (build 15+36)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 15+36, mixed mode, sharing)
“`
Granted, this is on Windows 10 and on a laptop without any thermal treatment / special benchmarking setup, and default VM options. I’ve tried twice, though, and QSON writer is always this slow. How can I help you diagnose the issue? I ran this because I wanted to try out json-iter and DSL-json and jackson-blackbird for comparison.
Dec 15, 2020 @ 23:34:18
Are you sure that you didn’t have any background processes screwing up the bench? Many of the numbers look out of whack from what I’ve been seeing. Granted, I’ve been benching on Fedora.
Dec 16, 2020 @ 00:28:02
Ok, yeah, that Jackson parser result was off. But the QSON writer results are completely consistent in all my runs today.
So here’s another run, this time with all other processes killed and me not touching the computer during the run:
Benchmark Mode Cnt Score Error Units
testParserDslJson thrpt 2 265835,448 ops/s
testParserJackson thrpt 2 195092,329 ops/s
testParserJacksonAfterburner thrpt 2 211729,538 ops/s
testParserJacksonBlackbird thrpt 2 195329,279 ops/s
testParserJsoniter thrpt 2 374728,557 ops/s
testParserQson thrpt 2 220883,170 ops/s
Benchmark Mode Cnt Score Error Units
testWriterDslJson thrpt 2 355928,682 ops/s
testWriterJackson thrpt 2 153220,738 ops/s
testWriterJacksonAfterburner thrpt 2 170941,233 ops/s
testWriterJacksonBlackbird thrpt 2 156555,109 ops/s
testWriterQson thrpt 2 62882,292 ops/s
I added Jsoniter (for parsing only), Jackson Blackbird and DSLjson (For this I had to “fix” raw generics in the Person2 class. The results are still representative.) for more comparison.
Now, I have *not* tried to look into what’s going on. I have not looked into JSON parser/writer configuration or VM options. I have not profiled nor looked at the generated code. For now I’m simply reporting what I see, and the biggest outlier today by far is QSON writer being slow. It may be a problem of Java 9+, or Windows, or my Intel processor, I do not know yet.
Dec 16, 2020 @ 05:13:16
Ok, take a look at master. I got rid of the underlying ByteArrayOutputStream and my bench numbers for qson almost doubled.
Dec 16, 2020 @ 19:32:44
OK, with other things being the same, QSON now shines with writing, too:
testParserQson thrpt 2 222259,232 ops/s
testWriterQson thrpt 2 223302,132 ops/s
Good job! This is now consistently the fastest with the exception of DSLjson (which generates bytecode, but has the most limiting functionality). I’ll try and share my code with you soon. Sorry for the delays, timezone difference and life, you know. I’ll get back to you asap, hopefully tomorrow, on Friday the latest.
Dec 16, 2020 @ 22:36:06
qson is all bytecode based too. Looked like DSL generated .java files as its an annotation processor…maybe not though.
Dec 16, 2020 @ 04:06:23
Can you share your JMH code for DSLjson? Also do you know if DSLjson generates bytecode or source code?
Apr 15, 2021 @ 15:26:45
Did you ever get to compare it to https://github.com/FasterXML/jackson-jr?
May 15, 2021 @ 20:57:32
Interesting numbers! Thank you for publishing this article.
About the only question I had was whether warmup and iteration counts are high enough — 2 seconds for warmup might be on low side for Jackson esp. with Afterburner? I usually use bit longer warmup when testing (4 or 5 seconds, see https://github.com/FasterXML/jackson-benchmarks/)
As to jackson-jr, from what I have seen on my own testing, its numbers are comparable to regular Jackson, depends on test case but have not seen big discrepancies.
Also: would it be ok to use test class(es)? Since test value class is bit bigger and varied than one I use for jackson-benchmarks, I’d be interested in doing some profiling to see why Afterburner deser case shows numbers so close to “vanilla” Jackson.
May 15, 2021 @ 22:02:32
Ah. Running the benchmark I realized that iteration length defaults to 10 seconds, not 1. So that should be plenty of time to get warmup etc. 🙂
And on reuse, I should be able to just run tests on profiler if need be.