50

We're considering replacing (some or many) 'classic' SOAP XML WCF calls by JSON (WCF or other) calls, because of the lower overhead and ease of use directly in Javascript. For now, we've just added an additional Json endpoint to our web service and added WebInvoke attributes to some operations and tested them. Everything works fine, using C# .Net clients or Javascript clients. So far so good.

However, it seems like deserializing big JSON strings to objects in C# .Net is much slower than deserializing SOAP XML. Both are using DataContract and DataMember attributes (exact same DTO). My question is: is this expected? Is there anything we can do to optimize this performance? Or should we consider JSON only for smaller requests where we DO notice performance improvements.

For now we've chosen JSON.net for this test and even though it doesn't show in this test case, it's supposed to be faster than the .Net JSON serialization. Somehow the ServiceStack deserialization does not work at all (no error, returns null for the IList).

For the test we do a service call to collect a list of rooms. It returns a GetRoomListResponse and in case of returning 5 dummy rooms, the JSON looks like this:

{"Acknowledge":1,"Code":0,"Message":null,"ValidateErrors":null,"Exception":null,"RoomList":[{"Description":"DummyRoom","Id":"205305e6-9f7b-4a6a-a1de-c5933a45cac0","Location":{"Code":"123","Description":"Location 123","Id":"4268dd65-100d-47c8-a7fe-ea8bf26a7282","Number":5}},{"Description":"DummyRoom","Id":"aad737f7-0caa-4574-9ca5-f39964d50f41","Location":{"Code":"123","Description":"Location 123","Id":"b0325ff4-c169-4b56-bc89-166d4c6d9eeb","Number":5}},{"Description":"DummyRoom","Id":"c8caef4b-e708-48b3-948f-7a5cdb6979ef","Location":{"Code":"123","Description":"Location 123","Id":"11b3f513-d17a-4a00-aebb-4d92ce3f9ae8","Number":5}},{"Description":"DummyRoom","Id":"71376c49-ec41-4b12-b5b9-afff7da882c8","Location":{"Code":"123","Description":"Location 123","Id":"1a188f13-3be6-4bde-96a0-ef5e0ae4e437","Number":5}},{"Description":"DummyRoom","Id":"b947a594-209e-4195-a2c8-86f20eb883c4","Location":{"Code":"123","Description":"Location 123","Id":"053e9969-d0ed-4623-8a84-d32499b5a8a8","Number":5}}]}

The Response and DTO's look like this:

[DataContract(Namespace = "bla")]
public class GetRoomListResponse
{
    [DataMember]
    public IList<Room> RoomList;

    [DataMember]
    public string Exception;

    [DataMember]
    public AcknowledgeType Acknowledge = AcknowledgeType.Success;

    [DataMember]
    public string Message;

    [DataMember]
    public int Code;

    [DataMember]
    public IList<string> ValidateErrors;
}

[DataContract(Name = "Location", Namespace = "bla")]
public class Location
{
    [DataMember]
    public Guid Id { get; set; }

    [DataMember]
    public int Number { get; set; }

    [DataMember]
    public string Code { get; set; }

    [DataMember]
    public string Description { get; set; }
}

[DataContract(Name = "Room", Namespace = "bla")]
public class Room
{
    [DataMember]
    public Guid Id { get; set; }

    [DataMember]
    public string Description { get; set; }

    [DataMember]
    public Location Location { get; set; }
}

Then our test code is as follows:

    static void Main(string[] args)
    {
        SoapLogin();

        Console.WriteLine();

        SoapGetRoomList();
        SoapGetRoomList();
        SoapGetRoomList();
        SoapGetRoomList();
        SoapGetRoomList();
        SoapGetRoomList();
        SoapGetRoomList();

        Console.WriteLine();

        JsonDotNetGetRoomList();
        JsonDotNetGetRoomList();
        JsonDotNetGetRoomList();
        JsonDotNetGetRoomList();
        JsonDotNetGetRoomList();
        JsonDotNetGetRoomList();
        JsonDotNetGetRoomList();

        Console.ReadLine();
    }

    private static void SoapGetRoomList()
    {
        var request = new TestServiceReference.GetRoomListRequest()
        {
            Token = Token,
        };

        Stopwatch sw = Stopwatch.StartNew();

        using (var client = new TestServiceReference.WARPServiceClient())
        {
            TestServiceReference.GetRoomListResponse response = client.GetRoomList(request);
        }

        sw.Stop();
        Console.WriteLine("SOAP GetRoomList: " + sw.ElapsedMilliseconds);
    }

    private static void JsonDotNetGetRoomList()
    {
        var request = new GetRoomListRequest()
        {
            Token = Token,
        };

        Stopwatch sw = Stopwatch.StartNew();
        long deserializationMillis;

        using (WebClient client = new WebClient())
        {
            client.Headers["Content-type"] = "application/json";
            client.Encoding = Encoding.UTF8;

            string requestData = JsonConvert.SerializeObject(request, JsonSerializerSettings);

            var responseData = client.UploadString(GetRoomListAddress, requestData);

            Stopwatch sw2 = Stopwatch.StartNew();
            var response = JsonConvert.DeserializeObject<GetRoomListResponse>(responseData, JsonSerializerSettings);
            sw2.Stop();
            deserializationMillis = sw2.ElapsedMilliseconds;
        }

        sw.Stop();
        Console.WriteLine("JSON.Net GetRoomList: " + sw.ElapsedMilliseconds + " (deserialization time: " + deserializationMillis + ")");
    }

    private static JsonSerializerSettings JsonSerializerSettings
    {
        get
        {
            var serializerSettings = new JsonSerializerSettings();

            serializerSettings.CheckAdditionalContent = false;
            serializerSettings.ConstructorHandling = ConstructorHandling.Default;
            serializerSettings.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat;
            serializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore;
            serializerSettings.NullValueHandling = NullValueHandling.Ignore;
            serializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace;
            serializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.None;
            serializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Error;

            return serializerSettings;
        }
    }

Now we've run this application with returning 50, 500 and 5000 rooms. The objects are not very complex.

These are the results; times are in ms:

50 rooms:

SOAP GetRoomList: 37
SOAP GetRoomList: 5
SOAP GetRoomList: 4
SOAP GetRoomList: 4
SOAP GetRoomList: 9
SOAP GetRoomList: 5
SOAP GetRoomList: 5

JSON.Net GetRoomList: 289 (deserialization time: 91)
JSON.Net GetRoomList: 3 (deserialization time: 0)
JSON.Net GetRoomList: 2 (deserialization time: 0)
JSON.Net GetRoomList: 2 (deserialization time: 0)
JSON.Net GetRoomList: 2 (deserialization time: 0)
JSON.Net GetRoomList: 2 (deserialization time: 0)
JSON.Net GetRoomList: 2 (deserialization time: 0)

500 rooms:

SOAP GetRoomList: 47
SOAP GetRoomList: 9
SOAP GetRoomList: 8
SOAP GetRoomList: 8
SOAP GetRoomList: 8
SOAP GetRoomList: 8
SOAP GetRoomList: 8

JSON.Net GetRoomList: 301 (deserialization time: 100)
JSON.Net GetRoomList: 12 (deserialization time: 8)
JSON.Net GetRoomList: 12 (deserialization time: 8)
JSON.Net GetRoomList: 12 (deserialization time: 8)
JSON.Net GetRoomList: 11 (deserialization time: 8)
JSON.Net GetRoomList: 11 (deserialization time: 8)
JSON.Net GetRoomList: 15 (deserialization time: 12)

5000 rooms:

SOAP GetRoomList: 93
SOAP GetRoomList: 51
SOAP GetRoomList: 58
SOAP GetRoomList: 60
SOAP GetRoomList: 53
SOAP GetRoomList: 53
SOAP GetRoomList: 51

JSON.Net GetRoomList: 405 (deserialization time: 175)
JSON.Net GetRoomList: 107 (deserialization time: 79)
JSON.Net GetRoomList: 108 (deserialization time: 82)
JSON.Net GetRoomList: 112 (deserialization time: 85)
JSON.Net GetRoomList: 105 (deserialization time: 79)
JSON.Net GetRoomList: 111 (deserialization time: 81)
JSON.Net GetRoomList: 110 (deserialization time: 82)

I'm running the application in release mode. Both client and server on same machine. As you can see, deserialization of many (of the same type of) objects takes much more time with JSON than the XML to object mapping that WCF SOAP uses. Hell, deserialization alone takes more time than the entire web service call using SOAP.

Is there an explanation for this? Does XML (or the WCF SOAP implementation) offer a big advantage in this area or are there any things I can change on the client side (I'd rather not change the service, but changing the client side DTO's is acceptable) to try to improve performance? It feels like I already selected some settings on the JSON.net side that should make it faster than default settings, no? What seems to be the bottleneck here?

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Colin B
  • 734
  • 1
  • 6
  • 13
  • I should perhaps have phrased the title better as now it may seem like a duplicate, but the other topic is a very generic question and I've actually tried two of the best performing ones (ServiceStack doesn't seem to work). I'm mainly interested in what causes the big difference in deserializing compared to SOAP XML and what would work best to improve performance in my situation. – Colin B Oct 15 '14 at 11:28
  • 2
    @ColinB by default ServiceStack Serializer only serializes **public properties**, where as your `GetRoomListResponse` DTO is using public **fields**. You can get ServiceStack to serialize public fields with: `JsConfig.IncludePublicFields = true`. – mythz Oct 15 '14 at 18:35
  • 1
    @ mythz thanks, that did the trick. It seems quite a bit faster than JSON.Net indeed, seems like their reputation of being the fastest is true here (though a bit slower than SOAP XML still for big lists). Like JSON.Net it seems like custom deserialization (that TheZencoder explained) is also possible with ServiceStack.Text, so when I have time, I'll expand my test suite and also include custom deserialized results for both ServiceStack and JSON.Net. – Colin B Oct 16 '14 at 08:16

4 Answers4

47

I have spent a little bit more time reading about JSON.NET internals, and my conclusion is that the slowness is caused mostly by reflection.

On the JSON.NET site i have found some nice performance tips, and i tried pretty much everything (JObject.Parse, Custom Converters etc.) but i couldn't squeeze out any significant performance improvement. Then i read the most important note on the whole site:

If performance is important and you don't mind more code to get it then this is your best choice. Read more about using JsonReader/JsonWriter here

So i listened to the advice and i implemented a basic version of a JsonReader to read the string efficiently:

var reader = new JsonTextReader(new StringReader(jsonString));

var response = new GetRoomListResponse();
var currentProperty = string.Empty;

while (reader.Read())
{
    if (reader.Value != null)
    {
        if (reader.TokenType == JsonToken.PropertyName)
            currentProperty = reader.Value.ToString();

        if (reader.TokenType == JsonToken.Integer && currentProperty == "Acknowledge")
            response.Acknowledge = (AcknowledgeType)Int32.Parse(reader.Value.ToString());

        if (reader.TokenType == JsonToken.Integer && currentProperty == "Code")
            response.Code = Int32.Parse(reader.Value.ToString());

        if (reader.TokenType == JsonToken.String && currentProperty == "Message")
            response.Message = reader.Value.ToString();

        if (reader.TokenType == JsonToken.String && currentProperty == "Exception")
            response.Exception = reader.Value.ToString();

        // Process Rooms and other stuff
    }
    else
    {
        // Process tracking the current nested element
    }
}

I think the exercise is clear, and without doubt this is the best performance you can get out of JSON.NET.

Just this limited code is 12x faster than the Deserialize version on my box with 500 rooms, but of course the mapping is not completed. However, i am pretty sure it will be at least 5x faster than deserialization in the worst-case.

Check out this link for more info about the JsonReader and how to use it:

http://james.newtonking.com/json/help/html/ReadingWritingJSON.htm

Ryan Emerle
  • 15,461
  • 8
  • 52
  • 69
Faris Zacina
  • 14,056
  • 7
  • 62
  • 75
  • I'll definitely look into the custom mapping, but I've tried replacing my current code with your suggestion, but in all cases it was actually slower. I'm using .Net Framework 4.0 on W7 and JSON.net 6.0.5. The first call was faster (Deserialize needs more time when initializing it seems), but each next call was slower. – Colin B Oct 15 '14 at 14:01
  • Hmm, actually, even the line 'var jsonObject = JObject.Parse(responseData);' alone seems about 20-25 % slower than DeserializeObject on my box, so custom mapping won't help if that line is needed first. – Colin B Oct 15 '14 at 14:08
  • Strange stuff. I will try to figure out something else that could work ;) – Faris Zacina Oct 15 '14 at 14:24
  • 1
    Try/Catch blocks cause a lot of slowness, so eliminate them. Reflection also is slow. For this, you can use [FastMember](http://www.nuget.org/packages/FastMember). – Icemanind Oct 15 '14 at 21:26
  • This seems great stuff, especially since in our SOAP XML code, we already do a custom mapping currently (from DTO to Model), so we could do both at once, going from JSON to Model in one go. I guess I should have looked further than just the basic examples, thanks! I'll work out the details later and post exact performance benefits, but I have no doubt that skipping reflection should make it a lot faster indeed. – Colin B Oct 16 '14 at 07:52
  • You are welcome. I also think this is the way to go. When you want performance you always go as low-level as possible :) – Faris Zacina Oct 16 '14 at 07:53
  • I guess it only works for simple objects. I tried this and my deserialization time grew 3x. – Igor Kulman Aug 31 '15 at 11:24
  • @IgorKulman it would be nice to see your code and measurements. Maybe you can create a new SO question. – Faris Zacina Aug 31 '15 at 13:15
  • This was only slightly faster for me; `0.22` seconds compared to `0.27` seconds for ~18k objects. – CrazyTim Jun 21 '18 at 00:08
  • yeah same, wasn't _that_ much faster, maybe about half...i need orders of magnitude – Ethan SK Nov 03 '22 at 23:37
5

I have now used the suggestions by both The ZenCoder and mythz and I have done more testing. I noticed an error in my first test setup as well, because while I built the tool in Release mode, I still started the test app from Visual Studio, which still added some debug overhead and this made a much bigger difference on the JSON.Net side compared to the SOAP XML side on my PC, so the difference in practice of the initial test results was quite a bit smaller already.

Either way, below are the results of collecting 5000 / 50000 rooms from the server (localhost), including mapping them to models.

5000 rooms:

----- Test results for JSON.Net (reflection) -----

GetRoomList (5000): 107
GetRoomList (5000): 60
GetRoomList (5000): 65
GetRoomList (5000): 62
GetRoomList (5000): 63

----- Test results for ServiceStack (reflection) -----

GetRoomList (5000): 111
GetRoomList (5000): 62
GetRoomList (5000): 62
GetRoomList (5000): 60
GetRoomList (5000): 62

----- Test results for SOAP Xml (manual mapping) -----

GetRoomList (5000): 101
GetRoomList (5000): 47
GetRoomList (5000): 51
GetRoomList (5000): 49
GetRoomList (5000): 51

----- Test results for Json.Net (manual mapping) -----

GetRoomList (5000): 58
GetRoomList (5000): 47
GetRoomList (5000): 51
GetRoomList (5000): 49
GetRoomList (5000): 47

----- Test results for ServiceStack (manual mapping) -----

GetRoomList (5000): 91
GetRoomList (5000): 79
GetRoomList (5000): 64
GetRoomList (5000): 66
GetRoomList (5000): 77

50000 rooms:

----- Test results for JSON.Net (reflection) -----

GetRoomList (50000): 651
GetRoomList (50000): 628
GetRoomList (50000): 642
GetRoomList (50000): 625
GetRoomList (50000): 628

----- Test results for ServiceStack (reflection) -----

GetRoomList (50000): 754
GetRoomList (50000): 674
GetRoomList (50000): 658
GetRoomList (50000): 657
GetRoomList (50000): 654

----- Test results for SOAP Xml (manual mapping) -----

GetRoomList (50000): 567
GetRoomList (50000): 556
GetRoomList (50000): 561
GetRoomList (50000): 501
GetRoomList (50000): 543

----- Test results for Json.Net (manual mapping) -----

GetRoomList (50000): 575
GetRoomList (50000): 569
GetRoomList (50000): 515
GetRoomList (50000): 539
GetRoomList (50000): 526

----- Test results for ServiceStack (manual mapping) -----

GetRoomList (50000): 850
GetRoomList (50000): 796
GetRoomList (50000): 784
GetRoomList (50000): 805
GetRoomList (50000): 768

Legend:

  • JSON.Net (reflection) -> JsonConvert.DeserializeObject (same JSON.Net code as above)
  • ServiceStack (reflection) -> JsonSerializer.DeserializeFromString
  • SOAP Xml (manual mapping) -> Same SOAP client call as above with added mapping from DTO's to models
  • JSON.Net (manual mapping) -> Mapping JSON to models directly using code based on The ZenCoder's code above, expanded to include mapping for the entire request (rooms and locations as well)

  • ServiceStack (manual mapping) -> See the below code (based on example: https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/UseCases/CentroidTests.cs)

            var response = JsonObject.Parse(responseData).ConvertTo(x => new GetRoomListResponse()
            {
                Acknowledge = (AcknowledgeType)x.Get<int>("Acknowledge"),
                Code = x.Get<int>("Code"),
                Exception = x.Get("Exception"),
                Message = x.Get("Message"),
                RoomList = x.ArrayObjects("RoomList").ConvertAll<RoomModel>(y => new RoomModel()
                {
                    Id = y.Get<Guid>("Id"),
                    Description = y.Get("Description"),
                    Location = y.Object("Location").ConvertTo<LocationModel>(z => new LocationModel()
                    {
                        Id = z.Get<Guid>("Id"),
                        Code = z.Get("Code"),
                        Description = z.Get("Description"),
                        Number = z.Get<int>("Number"),
                    }),
                }),
            });
    

Notes / personal conclusions:

  • Even reflection based deserialization is not that much slower than SOAP XML object generation in actual release mode (oops)
  • Manual mapping in JSON.Net is faster than the auto mapping and it is very comparable in speed to SOAP Xml mapping performance and it offers a lot of freedom, which is great, especially when models and DTO's differ in places
  • ServiceStack manual mapping is actually slower than their full reflection based mapping. I'm guessing this is because it's a higher level manual mapping than on the JSON.Net side, because some object generation seems to have already occurred there. Perhaps there are lower level alternatives on the ServiceStack side as well?
  • All this was done with server / client code running on same machine. In separate client / server production environments, I'm sure the JSON solutions should beat SOAP XML because of much smaller messages that need to be sent over the network
  • In this situation, JSON.Net auto mapping appears to be a tad faster than ServiceStack's for big responses.
Colin B
  • 734
  • 1
  • 6
  • 13
  • 1
    Just to mention that Yan has updated his Binary and JSON benchmarks (4/23/2015) : http://theburningmonk.com/2015/04/binary-and-json-benchmarks-updated-2 'HTH, – YSharp Apr 30 '15 at 21:38
3
var receivedObject = JsonConvert.DeserializeObject<dynamic>(content);

works much faster for me then:

var receivedObject = JsonConvert.DeserializeObject<Product>(content);

and this is even faster:

dynamic receivedObject = JObject.Parse(content); // The same goes for JArray.Parse()
tedi
  • 6,350
  • 5
  • 52
  • 67
0

I'm adding 2 more points here, which help me to improve my IoT application performance. I was receiving millions of JSON messages every day.

  1. Changes in ContractResolver instance

Old code

return JsonConvert.SerializeObject(this, Formatting.Indented,
                          new JsonSerializerSettings
                          {
                              ContractResolver = new CamelCasePropertyNamesContractResolver()
                          });

New Code

Not creating contract resolver instance on every call, Instead using a single instance

return JsonConvert.SerializeObject(this, Formatting.Indented,
                          new JsonSerializerSettings
                          {
                              ContractResolver = AppConfiguration.CamelCaseResolver
                          });
  1. Avoid creating JObject

Old Code

JObject eventObj = JObject.Parse(jsonMessage);
eventObj.Add("AssetType", assetType); //modify object

JObject eventObj2 = JObject.Parse(jsonMessage);
eventObj.Add("id", id); //modify object

NewCode

JObject eventObj = JObject.Parse(jsonMessage);
eventObj.Add("AssetType", assetType); //modify object

JObject eventObj2 = (JObject)eventObj.DeepClone();
eventObj.Add("id", id); //modify object

To check performance benefits, I used benchmarkdotnet to see the difference. check this link as well.

Pankaj Rawat
  • 4,037
  • 6
  • 41
  • 73