As you might be aware, SphinxConnector.NET’s fluent API uses JSON.NET as
its default JSON serializer when dealing with JSON attributes. As the
old saying goes “No one has ever been fired for using JSON.NET” 😉
Choosing JSON.NET was obvious, because it has been the de-facto standard
in the .NET world since, well, forever. Even Microsoft adopted it as the
default library for handling JSON in ASP.NET (MVC/Web API), so it’s
probably not even an additional dependency for most people, which is a
bonus.
There are however a bunch of other JSON libraries out there that do
better in terms of performance and memory usage than JSON.NET, so I
decided to test some them with real world data of a customer. I’ll be
comparing
to JSON.NET 12.0.1.
Switching JSON serializers in SphinxConnector.NET’s fluent API is pretty
easy, just inherit from JsonObjectSerializer, implement Serialize
and Deserialize, register your class by setting the
JsonObjectSerializer property and you are good to go. Here’s how the
ServiceStackJsonSerializer looks
like:
class ServiceStackJsonSerializer : JsonObjectSerializer
{
public override string Serialize(object obj) => ServiceStack.Text.JsonSerializer.SerializeToString(obj);
public override object Deserialize(string json, Type type) => ServiceStack.Text.JsonSerializer.DeserializeFromString(json, type);
}
As mentioned earlier, I’m using real world data that is used in
production and is pretty JSON heavy. For the benchmark I’m just querying
the JSON data, to better isolate the effect of using the different
libraries with regards to execution speed (Op/s). Here are the results
(data queried from Manticore 2.8.2):
BenchmarkDotNet=v0.11.5, OS=Windows 7 SP1 (6.1.7601.0)
Intel Core i5-3570 CPU 3.40GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores
Frequency=3420566 Hz, Resolution=292.3493 ns, Timer=TSC
[Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3324.0
Job-FEFVFA : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3324.0
InvocationCount=1 UnrollFactor=1
| Method |
Mean |
Error |
StdDev |
Op/s |
Ratio |
Gen 0 |
Gen 1 |
Gen 2 |
Allocated |
| JsonNet |
205.2 ms |
4.617 ms |
7.456 ms |
4.874 |
1.00 |
3000.0 |
1000.0 |
- |
13.75 MB |
| ServiceStack.Text |
195.9 ms |
1.734 ms |
1.537 ms |
5.104 |
0.94 |
1000.0 |
- |
- |
7.24 MB |
| Utf8Json |
190.9 ms |
1.335 ms |
1.249 ms |
5.239 |
0.92 |
2000.0 |
- |
- |
9.45 MB |
| JilJson |
190.0 ms |
1.263 ms |
1.182 ms |
5.262 |
0.92 |
3000.0 |
1000.0 |
- |
14.44 MB |
The results are pretty impressive: Servicestack.Text is using nearly 50%
less memory, causes less garbage collections and is also slightly faster
than JSON.NET*****. Utf8Json is even faster than Servicestack.Text but
uses more memory. Jil performs best, albeit slightly, but does so at the
cost of an even higher memory consumption than JSON.NET, which isn’t
surprising as this an
intentional design decision.
Could we do even better?
We can. In SphinxConnector.NET 4.2 we'll be adding two more overloads to
the Deserialize method:
public virtual object Deserialize(ReadOnlyMemory<byte> json, Type type)
public virtual object Deserialize(ReadOnlyMemory<char> json, Type type)
Instead of passing a string containing the JSON data, we are passing an
instance of ReadOnlyMemory of char or byte. This way, we’re avoiding
the creation of an intermediate string and instead pass on the raw data
we’ve read from the socket.
ServiceStack.Text supports deserializing JSON from
ReadOnlyMemory<char>, Utf8Json accepts an array of bytes. Jil and
JSON.NET don’t offer any methods to that work directly on chars or
bytes, here we need to create a MemoryStream over
ReadOnlyMemory<byte> which in turn gets passed into a TextReader
which can then be passed to the corresponding Deserialize methods.
| Method |
Mean |
Error |
StdDev |
Op/s |
Ratio |
Gen 0 |
Gen 1 |
Gen 2 |
Allocated |
| JsonNet |
196.9 ms |
2.050 ms |
1.8170 ms |
5.078 |
1.00 |
3000.0 |
1000.0 |
- |
13.25 MB |
| ServiceStack.Text |
191.3 ms |
1.119 ms |
0.9346 ms |
5.228 |
0.97 |
1000.0 |
- |
- |
5.56 MB |
| Utf8Json |
189.4 ms |
1.088 ms |
1.0177 ms |
5.279 |
0.96 |
2000.0 |
- |
- |
7.75 MB |
| Jil |
193.2 ms |
1.295 ms |
1.2116 ms |
5.175 |
0.98 |
3000.0 |
1000.0 |
- |
13.93 MB |
After eliminating the intermediate string allocations and passing our
chars/bytes directly to ServiceStack.Text / Utf8Json we can observe a
further reduction in memory usage. JSON.NET and Jil see some
improvement, but not as much as the other two. JSON.NET also is faster
than before in terms of Op/s whereas Jil is slightly slower.
* The performance advantages of the tested libraries compared to
JSON.NET don’t show that much in a benchmark like this, because there’s
much more going on here than just deserializing JSON data (query
execution, network transmission etc.). If you’d just compare the
deserialization process, you’d see much more of a speed advantage these
libraries provide (check out the benchmarks on the respective project
sites).
Conclusion
Replacing JSON.NET with an alternative JSON serializer in
SphinxConnector.NET’s fluent API can lead to a significant reduction in
memory usage and a nice improvement in speed. Given that this can be
done with just a few lines of code, this is something I’d seriously look
into for applications that make use of JSON attributes. On a related
note, I’d also consider replacing JSON.NET within ASP.NET MVC/Web API in
case your application is build with these.