web-dev-qa-db-fra.com

Comment améliorer la vitesse de désérialisation JSON dans .Net? (JSON.net ou autre?)

Nous envisageons de remplacer (certains ou beaucoup) les appels WCF XML XML «classiques» SOAP par des appels JSON (WCF ou autres), en raison de la surcharge de temps et de la facilité d'utilisation directement en Javascript. Pour l'instant, nous venons d'ajouter un point de terminaison Json supplémentaire à notre service Web et avons ajouté des attributs WebInvoke à certaines opérations et les avons testées. Tout fonctionne bien, en utilisant des clients C # .Net ou des clients Javascript. Jusqu'ici tout va bien.

Cependant, il semble que la désérialisation de grandes chaînes JSON en objets en C # .Net est beaucoup plus lente que la désérialisation de XML SOAP. Les deux utilisent les attributs DataContract et DataMember (exactement le même DTO). Ma question est la suivante: est-ce attendu? Pouvons-nous faire quelque chose pour optimiser cette performance? Ou devrions-nous envisager JSON uniquement pour les petites demandes pour lesquelles nous constatons des améliorations des performances.

Pour l'instant, nous avons choisi JSON.net pour ce test et même s'il ne s'affiche pas dans ce scénario, il est supposé être plus rapide que la sérialisation .Net JSON. D'une manière ou d'une autre, la désérialisation de ServiceStack ne fonctionne pas du tout (pas d'erreur, retourne null pour IList).

Pour le test, nous faisons un appel de service pour rassembler une liste de chambres. Il retourne une GetRoomListResponse et en cas de retour de 5 salles factices, le JSON ressemble à ceci:

{"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 réponse et les DTO ressemblent à ceci:

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

Alors notre code de test est le suivant:

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

Maintenant, nous avons exécuté cette application avec 50, 500 et 5000 salles restituées. Les objets ne sont pas très complexes.

Ce sont les résultats; les temps sont en ms:

50 chambres:

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

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

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)

J'exécute l'application en mode release. Le client et le serveur sur le même ordinateur. Comme vous pouvez le constater, la désérialisation de nombreux objets (du même type d'objets) prend beaucoup plus de temps avec JSON que le mappage XML-objet utilisé par WCF SOAP. Enfer, la désérialisation prend à elle seule plus de temps que l'appel de tout le service Web utilisant SOAP.

Y a-t-il une explication à cela? XML (ou l’implémentation WCF SOAP) offre-t-il un avantage considérable dans ce domaine ou puis-je modifier quelque chose du côté client (je préférerais ne pas changer le service, mais changer le DTO côté client est acceptable ) pour essayer d’améliorer les performances? C'est comme si j'avais déjà sélectionné certains paramètres du côté JSON.net qui devraient le rendre plus rapide que les paramètres par défaut, non? Quel semble être le goulot d'étranglement ici?

37
Colin B

J'ai passé un peu plus de temps à lire sur les composants internes de JSON.NET et ma conclusion est que la lenteur est principalement causée par réflexion

Sur le site JSON.NET, j’ai trouvé quelques astuces sur les performances optimales , et j’essayais à peu près tout (JObject.Parse, convertisseurs personnalisés, etc.), mais je n’ai pas réussi à obtenir une amélioration significative des performances. Ensuite, j'ai lu la note la plus importante sur l'ensemble du site:

Si les performances sont importantes et que vous ne voulez pas que plus de code l’obtienne, c’est votre meilleur choix. En savoir plus sur l'utilisation de JsonReader/JsonWriter ici

J'ai donc écouté les conseils et j'ai implémenté une version de base d'un JsonReader pour lire efficacement la chaîne:

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

Je pense que l'exercice est clair et que c'est sans aucun doute la meilleure performance que vous puissiez obtenir avec JSON.NET.

Seulement ce code limité est 12 fois plus rapide que la version Deserialize de mon boîtier de 500 pièces, mais bien sûr, le mappage n’est pas terminé. Cependant, je suis à peu près sûr que ce sera au moins 5 fois plus rapide que la désérialisation dans le pire des cas.

Consultez ce lien pour plus d’informations sur le JsonReader et son utilisation:

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

32
Faris Zacina

J'ai maintenant utilisé les suggestions de The ZenCoder et de Mythz et j'ai effectué davantage de tests. J'ai également remarqué une erreur dans la configuration de mon premier test car, alors que je construisais l'outil en mode Release, je lançais toujours l'application de test à partir de Visual Studio, ce qui ajoutait encore un temps de débogage supplémentaire, ce qui faisait une différence beaucoup plus grande sur JSON.Net. Par rapport au côté XML SOAP de mon PC, la différence dans la pratique des résultats du test initial était déjà un peu plus petite.

Dans les deux cas, vous trouverez ci-dessous les résultats de la collecte de 5000/50000 pièces sur le serveur (localhost), y compris leur mappage avec des modèles.

5000 chambres:

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

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

Légende:

  • JSON.Net (réflexion) -> JsonConvert.DeserializeObject (même code JSON.Net que ci-dessus)
  • ServiceStack (réflexion) -> JsonSerializer.DeserializeFromString
  • SOAP Xml (mappage manuel) -> Le même appel client SOAP que ci-dessus avec un mappage ajouté des DTO aux modèles
  • JSON.Net (mappage manuel) -> Mappage JSON sur des modèles directement à l'aide d'un code basé sur le code de ZenCoder ci-dessus, élargi pour inclure le mappage de l'ensemble de la demande (salles et emplacements également)

  • ServiceStack (mappage manuel) -> Voir le code ci-dessous (basé sur l'exemple: 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/conclusions personnelles:

  • Même la désérialisation basée sur la réflexion n’est pas beaucoup plus lente que la génération d’objets XML SOAP en mode réel (oops)
  • Le mappage manuel dans JSON.Net est plus rapide que le mappage automatique et sa vitesse est très comparable aux performances de mappage Xml SOAP. Il offre une grande liberté, ce qui est formidable, en particulier lorsque les modèles et les DTO diffèrent par endroit.
  • Le mappage manuel de ServiceStack est en réalité plus lent que leur mappage basé sur la réflexion complète. J'imagine que c'est parce qu'il s'agit d'un mappage manuel de niveau supérieur à celui du côté JSON.Net, car une certaine génération d'objets semble déjà s'être produite là-bas. Peut-être existe-t-il également des alternatives de niveau inférieur du côté de ServiceStack?
  • Tout cela a été fait avec le code serveur/client s'exécutant sur le même ordinateur. Dans des environnements de production client/serveur distincts, je suis sûr que les solutions JSON doivent battre le code XML SOAP en raison du nombre beaucoup plus petit de messages qui doivent être envoyés sur le réseau.
  • Dans cette situation, le mappage automatique JSON.Net semble être un peu plus rapide que celui de ServiceStack pour les réponses volumineuses.
4
Colin B
var receivedObject = JsonConvert.DeserializeObject<dynamic>(content);

travaille beaucoup plus vite pour moi alors:

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

et c'est encore plus rapide:

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

J'ajoute 2 points supplémentaires ici, ce qui m'aide à améliorer les performances de mes applications IoT. Je recevais des millions de messages JSON chaque jour.

  1. Modifications dans l'instance ContractResolver

Ancien code

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

Nouveau code

return JsonConvert.SerializeObject(this, Formatting.Indented,
                          new JsonSerializerSettings
                          {
                              ContractResolver = AppConfiguration.CamelCaseResolver
                          });
  1. Évitez de créer JObject

Ancien 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

Pour vérifier les avantages en termes de performances, j’ai utilisé benchmarkdotnet pour voir la différence. vérifier ce lien aussi.

0
Pankaj Rawat