¿Cómo mejorar la velocidad de deserialización JSON en .Net? (JSON.net u otro?)

Estamos considerando reemplazar (algunas o muchas) llamadas “clásicas” de WCF XML WCF por llamadas JSON (WCF u otras), debido a la menor sobrecarga y la facilidad de uso directamente en Javascript. Por ahora, acabamos de agregar un punto final Json adicional a nuestro servicio web, agregamos atributos de WebInvoke a algunas operaciones y los probamos. Todo funciona bien, utilizando clientes de C # .Net o clientes de Javascript. Hasta ahora tan bueno.

Sin embargo, parece que deserializar grandes cadenas JSON a objetos en C # .Net es mucho más lento que deserializar SOAP XML. Ambos están utilizando los atributos DataContract y DataMember (exactamente la misma DTO). Mi pregunta es: ¿se espera esto? ¿Hay algo que podamos hacer para optimizar este rendimiento? O deberíamos considerar JSON solo para solicitudes más pequeñas en las que SI notamos mejoras en el rendimiento.

Por ahora, hemos elegido JSON.net para esta prueba y aunque no se muestra en este caso de prueba, se supone que es más rápido que la serialización .Net JSON. De alguna manera, la deserialización de ServiceStack no funciona en absoluto (sin error, devuelve nulo para el IList).

Para la prueba hacemos una llamada de servicio para recoger una lista de habitaciones. Devuelve un GetRoomListResponse y en el caso de devolver 5 habitaciones ficticias, el JSON tiene este aspecto:

{"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}}]} 

La respuesta y el DTO se ven así:

 [DataContract(Namespace = "bla")] public class GetRoomListResponse { [DataMember] public IList RoomList; [DataMember] public string Exception; [DataMember] public AcknowledgeType Acknowledge = AcknowledgeType.Success; [DataMember] public string Message; [DataMember] public int Code; [DataMember] public IList 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; } } 

Entonces nuestro código de prueba es el siguiente:

  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(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; } } 

Ahora hemos ejecutado esta aplicación con 50, 500 y 5000 habitaciones que regresan. Los objetos no son muy complejos.

Estos son los resultados; los tiempos están en ms:

50 habitaciones:

 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 habitaciones:

 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 habitaciones:

 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) 

Estoy ejecutando la aplicación en modo de lanzamiento. Tanto cliente como servidor en la misma máquina. Como puede ver, la deserialización de muchos objetos (del mismo tipo de) lleva mucho más tiempo con JSON que con la asignación de XML a objeto que utiliza WCF SOAP. Demonios, solo la deserialización lleva más tiempo que toda la llamada del servicio web utilizando SOAP.

¿Hay alguna explicación para esto? ¿XML (o la implementación de WCF SOAP) ofrece una gran ventaja en esta área o hay algunas cosas que puedo cambiar en el lado del cliente (preferiría no cambiar el servicio, pero cambiar el DTO del lado del cliente es aceptable) para tratar de ¿mejorar el rendimiento? Parece que ya seleccioné algunas configuraciones en el lado de JSON.net que deberían hacerlo más rápido que las configuraciones predeterminadas, ¿no? ¿Qué parece ser el cuello de botella aquí?

He pasado un poco más de tiempo leyendo sobre los aspectos internos de JSON.NET, y mi conclusión es que la lentitud se debe principalmente a la reflexión .

En el sitio de JSON.NET he encontrado algunos buenos consejos de rendimiento , y probé casi todo (JObject.Parse, convertidores personalizados, etc.) pero no pude exprimir ninguna mejora significativa en el rendimiento. Luego leí la nota más importante de todo el sitio:

Si el rendimiento es importante y no le importa más código para obtenerlo, entonces esta es su mejor opción. Lea más sobre el uso de JsonReader / JsonWriter aquí

Así que escuché el consejo e implementé una versión básica de un JsonReader para leer la cadena de manera eficiente:

 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 } } 

Creo que el ejercicio es claro y, sin duda, este es el mejor rendimiento que puede obtener de JSON.NET .

Solo este código limitado es 12 veces más rápido que la versión Deserialize en mi caja con 500 habitaciones, pero, por supuesto, el mapeo no está completo. Sin embargo, estoy bastante seguro de que será al menos 5 veces más rápido que la deserialización en el peor de los casos.

Echa un vistazo a este enlace para obtener más información sobre el JsonReader y cómo usarlo:

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

Ahora he usado las sugerencias tanto de The ZenCoder como de mythz y he realizado más pruebas. También noté un error en mi primera configuración de prueba, ya que mientras construía la herramienta en el modo de lanzamiento, aún empecé la aplicación de prueba de Visual Studio, que aún agregaba algo de sobrecarga de depuración y esto marcó una diferencia mucho mayor en JSON.Net En comparación con el lado SOAP XML de mi PC, la diferencia en la práctica de los resultados de las pruebas iniciales ya era bastante más pequeña.

De cualquier manera, a continuación se muestran los resultados de la recostackción de 5000/50000 habitaciones del servidor (localhost), incluida su asignación a los modelos.

5000 habitaciones:

 ----- 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 habitaciones:

 ----- 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 

Leyenda:

  • JSON.Net (reflexión) -> JsonConvert.DeserializeObject (el mismo código JSON.Net anterior)
  • ServiceStack (reflexión) -> JsonSerializer.DeserializeFromString
  • SOAP Xml (asignación manual) -> La misma llamada de cliente SOAP que la anterior con asignación adicional de DTO a modelos
  • JSON.Net (mapeo manual) -> Mapeo de JSON a modelos directamente usando código basado en el código de ZenCoder anterior, ampliado para incluir mapeo para toda la solicitud (habitaciones y ubicaciones también)

  • ServiceStack (asignación manual) -> Vea el siguiente código (basado en el ejemplo: 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("Acknowledge"), Code = x.Get("Code"), Exception = x.Get("Exception"), Message = x.Get("Message"), RoomList = x.ArrayObjects("RoomList").ConvertAll(y => new RoomModel() { Id = y.Get("Id"), Description = y.Get("Description"), Location = y.Object("Location").ConvertTo(z => new LocationModel() { Id = z.Get("Id"), Code = z.Get("Code"), Description = z.Get("Description"), Number = z.Get("Number"), }), }), }); 

Notas / conclusiones personales:

  • Incluso la deserialización basada en la reflexión no es mucho más lenta que la generación de objetos SOAP XML en el modo de publicación real (oops)
  • El mapeo manual en JSON.Net es más rápido que el mapeo automático y es muy comparable en velocidad al rendimiento del mapeo SOAP Xml y ofrece mucha libertad, lo cual es genial, especialmente cuando los modelos y DTO difieren en algunos lugares
  • La asignación manual de ServiceStack es en realidad más lenta que su asignación basada en la reflexión completa. Supongo que esto se debe a que es un mapeo manual de mayor nivel que en el lado de JSON.Net, porque parece que ya se ha producido alguna generación de objetos allí. ¿Quizás también hay alternativas de nivel inferior en el lado de ServiceStack?
  • Todo esto se hizo con el código del servidor / cliente ejecutándose en la misma máquina. En entornos de producción de cliente / servidor separados, estoy seguro de que las soluciones JSON deberían superar a SOAP XML debido a que los mensajes mucho más pequeños deben enviarse a través de la red
  • En esta situación, la asignación automática JSON.Net parece ser un poco más rápida que la de ServiceStack para grandes respuestas.
 var receivedObject = JsonConvert.DeserializeObject(content); 

trabaja mucho más rápido para mí entonces:

 var receivedObject = JsonConvert.DeserializeObject(content); 

y esto es aún más rápido:

 dynamic receivedObject = JObject.Parse(content); // The same goes for JArray.Parse()