Search code examples
c#.netjsonperformancejson.net

How to improve JSON deserialization speed in .Net? (JSON.net or other?)


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?


Solution

  • 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