2014-10-15 11 views
31

Zastanawiamy się nad zastąpieniem (niektórych lub wielu) "klasycznych" wywołań WCF SOAP XML WCF przez wywołania JSON (WCF lub inne) ze względu na niższe obciążenie i łatwość użycia bezpośrednio w JavaScript. Na razie dodaliśmy dodatkowy punkt końcowy Json do naszej usługi internetowej i dodaliśmy atrybuty WebInvoke do niektórych operacji i przetestowaliśmy je. Wszystko działa dobrze, używając klientów C# .Net lub klientów Javascript. Jak na razie dobrze.Jak poprawić szybkość deserializacji JSON w .Net? (JSON.net lub inny?)

Jednak wygląda na to, że następuje deserializacja dużych łańcuchów JSON do obiektów w języku C# .Net jest znacznie wolniejsza niż deserializacja protokołu SOAP XML. Oba używają atrybutów DataContract i DataMember (dokładnie to samo DTO). Moje pytanie brzmi: czy to jest oczekiwane? Czy jest coś, co możemy zrobić, aby zoptymalizować tę wydajność? Czy powinniśmy rozważyć JSON tylko w przypadku mniejszych wniosków, w których zauważamy poprawę wydajności.

Na razie wybraliśmy JSON.net dla tego testu i mimo że nie jest on wyświetlany w tym przypadku testowym, powinien być szybszy niż serializacja .NET JSON. W jakiś sposób deserializacja ServiceStack w ogóle nie działa (brak błędu, zwraca wartość pustą dla IList).

Do testu wykonujemy wywołanie serwisowe w celu zebrania listy pokoi. Zwraca GetRoomListResponse aw przypadku zwrotu 5 pokoi obojętne, JSON wygląda następująco:

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

odpowiedź i wygląd dto jest tak:

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

Wtedy nasz kod test jest w następujący sposób:

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

Teraz uruchomiliśmy tę aplikację, zwracając 50, 500 i 5000 pokoi. Obiekty nie są zbyt skomplikowane.

Oto wyniki; czasy w ms:

50 pokoje:

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

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

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) 

biegnę aplikacji w trybie uwolnienia. Zarówno klient, jak i serwer na tej samej maszynie. Jak widać, deserializacja wielu obiektów tego samego typu zajmuje dużo więcej czasu przy użyciu JSON, niż w przypadku XML w celu mapowania obiektów, które korzysta z WCF SOAP. Piekło, sama deserializacja zajmuje więcej czasu niż całe wywołanie usługi sieciowej za pomocą SOAP.

Czy istnieje wyjaśnienie tego? Czy XML (lub implementacja SOAP WCF) oferuje dużą przewagę w tym obszarze, czy są rzeczy, które mogę zmienić po stronie klienta (wolę nie zmieniać usługi, ale zmiana DTO po stronie klienta jest akceptowalna), aby spróbować Polepsz wykonanie? Czuję, że już wybrałem niektóre ustawienia strony JSON.net, które powinny sprawić, że będzie to szybsze niż ustawienia domyślne, nie? Co wydaje się tutaj wąskim gardłem?

+0

Powinienem może być sformułowany tytuł lepiej jak teraz może wydawać się duplikat, ale drugi temat to bardzo ogólne pytanie i faktycznie wypróbowałem dwa z najbardziej wydajnych (ServiceStack nie działa). Interesuje mnie głównie to, co powoduje dużą różnicę w deserializacji w porównaniu z XML SOAP i co najlepiej działa, by poprawić wydajność w mojej sytuacji. –

+2

@ColinB domyślnie ServiceStack Serializer serializuje tylko ** właściwości publiczne **, gdzie jako twój DTO 'GetRoomListResponse' używa publicznych ** pól **. Możesz uzyskać ServiceStack do serializacji pól publicznych za pomocą: 'JsConfig.IncludePublicFields = true'. – mythz

+1

@ Mythz dzięki, to załatwiło sprawę. Wydaje się, że jest nieco szybszy niż JSON.Net, wydaje się, że ich reputacja jest najszybsza, jest tutaj prawdziwa (choć nieco wolniejsza niż XML SOAP wciąż dla dużych list). Podobnie jak JSON.Net wygląda na to, że niestandardowa deserializacja (wyjaśniona przez TheZencoder) jest również możliwa dzięki ServiceStack.Text, więc gdy będę mieć czas, poszerzę swój pakiet testowy i uwzględnię niestandardowe deserializowane wyniki zarówno dla ServiceStack, jak i JSON.Net. –

Odpowiedz

26

Spędziłem trochę więcej czasu na czytaniu o wewnętrznych obiektach JSON.NET, a mój wniosek jest taki, że spowolnienie spowodowane jest głównie przez odbicie.

Na stronie JSON.NET znalazłem trochę nice performance tips i próbowałem prawie wszystkiego (JObject.Parse, niestandardowe konwertery itp.), ale nie mogłem wycisnąć żadnej znaczącej poprawy wydajności. Następnie przeczytałem najważniejszą notatkę na całej stronie:

Jeśli wydajność jest ważna i nie masz nic przeciwko, aby uzyskać więcej kodu, to jest to najlepszy wybór. Czytaj więcej na temat korzystania JsonReader/JsonWriter tutaj

Więc ja słuchałem rad i wdrożone podstawową wersję JsonReader czytać ciąg sprawnie:

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

Myślę, że ćwiczenia są jasne i bez wątpienia jest to najlepsza wydajność, jaką można uzyskać z JSON.NET.

Ten ograniczony kod jest 12 razy szybszy niż wersja Deserialize na moim pudełku z 500 pokojami, ale oczywiście mapowanie nie zostało zakończone. Jestem jednak prawie pewien, że będzie to co najmniej 5 razy szybsze niż deserializacja w najgorszym przypadku.

Sprawdź ten link, aby uzyskać więcej informacji na temat JsonReader i jak go używać:

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

+0

Z pewnością zajrzę do niestandardowego mapowania, ale próbowałem zastąpić mój obecny kod sugestią, ale we wszystkich przypadkach było to wolniejsze. Używam .Net Framework 4.0 na W7 i JSON.net 6.0.5. Pierwsze wywołanie było szybsze (Deserializacja wymaga więcej czasu, gdy inicjowanie się wydaje), ale każde kolejne połączenie było wolniejsze. –

+0

Hmm, właściwie nawet linia 'var jsonObject = JObject.Parse (responseData);' sam wydaje się o 20-25% wolniejszy niż DeserializeObject na moim pudełku, więc niestandardowe mapowanie nie pomoże, jeśli linia ta będzie potrzebna jako pierwsza. –

+0

Dziwne rzeczy.Spróbuję wymyślić coś innego, co mogłoby zadziałać;) –

2

Mam teraz używany propozycje zarówno przez ZenCoder i mythz i zrobić więcej badań. Zauważyłem także błąd w mojej pierwszej konfiguracji testowej, ponieważ podczas budowania tego narzędzia w trybie Release, nadal uruchamiałem aplikację testową z Visual Studio, która wciąż dodawała pewne koszty debugowania i to znacznie zwiększało różnicę w JSON.Net strony w porównaniu do strony XML SOAP na moim komputerze, więc różnica w praktyce początkowych wyników testów była już nieco mniejsza.

Tak czy inaczej, poniżej przedstawiono wyniki zbierania 5000/50000 pokoi z serwera (localhost), w tym mapowanie ich do modeli.

5000 pokoje:

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

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

Legenda:

  • JSON.Net (odbicie) -> JsonConvert.DeserializeObject (sama Json.NET kod jak wyżej)
  • ServiceStack (odbicie) -> JsonSerializer.DeserializeFromString
  • SOAP XML (instrukcja mapping) -> Same SOAP wywołanie klient jak wyżej z dodatkiem mapowania z DTO do modeli
  • JSON.Netto (ręczne mapowanie) -> Mapowanie JSON do modeli bezpośrednio za pomocą kodu na podstawie kodu na ZenCoder za wyżej, poszerzony odwzorowanie dla całego życzenie (w pomieszczeniach i miejscach, jak również)

  • ServiceStack (ręczne mapowanie) - > Zobacz poniższy kod (oparte na przykład: 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"), 
           }), 
          }), 
         }); 
    

Uwagi/osobiste wnioski:

  • Nawet deserializacja oparta na refleksach nie jest dużo wolniejsza niż generowanie obiektów XML SOAP w trybie faktycznego wydania (oops)
  • Mapowanie ręczne w JSON.Net jest szybsze niż automatyczne mapowanie i jest bardzo porównywalne pod względem wydajności mapowania SOAP Xml i oferuje dużo swobody, co jest wspaniałe, zwłaszcza gdy modele i DTO różnią się miejscami:
  • Mapowanie manualne ServiceStack jest wolniejsze niż ich pełne odwzorowanie oparte na odbiciu. Zgaduję, że to dlatego, że jest to mapowanie ręczne wyższego poziomu niż po stronie JSON.Net, ponieważ wydaje się, że pewne generowanie obiektów już tam zaszło. Być może istnieją również alternatywy niższego poziomu po stronie ServiceStack?
  • Wszystko to zostało wykonane przy użyciu kodu serwera/klienta działającego na tym samym komputerze. W oddzielnych środowiskach produkcyjnych klient/serwer, jestem pewien, że rozwiązania JSON powinny pokonać protokół SOAP XML ze względu na znacznie mniejsze wiadomości, które muszą zostać wysłane przez sieć.
  • W tej sytuacji automatyczne mapowanie JSON.Net wydaje się być odrobinę szybsze niż ServiceStack dla dużych odpowiedzi.
+1

Wystarczy wspomnieć, że Yan zaktualizował swoje testy porównawcze Binary i JSON (4/23/2015): http://theburningmonk.com/2015/04/binary-and-json-benchmarks-updated-2 "HTH , – YSharp

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

działa znacznie szybciej dla mnie wtedy:

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

i to jeszcze szybciej:

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