Как улучшить скорость десериализации JSON в .Net? (JSON.net или другое?)
Мы рассматриваем возможность замены (некоторых или многих) "классических" SOAP-XML-вызовов WCF вызовами JSON (WCF или другими) из-за более низких издержек и простоты использования непосредственно в Javascript. На данный момент мы добавили дополнительную конечную точку Json в нашу веб-службу и добавили атрибуты WebInvoke для некоторых операций и протестировали их. Все работает отлично, используя клиентов С#.Net или Javascript. Пока все хорошо.
Однако, похоже, десериализация больших строк JSON для объектов в С#.Net намного медленнее десериализации SOAP XML. Оба используют атрибуты DataContract и DataMember (тот же самый DTO). Мой вопрос: это ожидается? Можно ли что-то сделать, чтобы оптимизировать эту производительность? Или мы должны рассматривать JSON только для небольших запросов, где мы обращаем внимание на улучшение производительности.
Пока мы выбрали JSON.net для этого теста, и хотя он не показан в этом тестовом примере, он должен быть быстрее, чем сериализация .Net JSON. Как-то десериализация ServiceStack вообще не работает (нет ошибки, возвращает null для IList).
Для тестирования мы выполняем служебный вызов для сбора списка номеров. Он возвращает GetRoomListResponse, и в случае возвращения 5 манекенов, JSON выглядит так:
{"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}}]}
Ответ и DTO выглядят следующим образом:
[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; }
}
Тогда наш тестовый код выглядит следующим образом:
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;
}
}
Теперь мы запустили это приложение с возвратом 50, 500 и 5000 номеров. Объекты не очень сложны.
Это результаты; times находятся в ms:
50 номеров:
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 номеров:
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 номеров:
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)
Я запускаю приложение в режиме выпуска. И клиент, и сервер на одной машине. Как вы можете видеть, десериализация многих объектов одного и того же типа занимает гораздо больше времени с JSON, чем сопоставление XML с объектами, используемое WCF SOAP. Ад, десериализация сама по себе занимает больше времени, чем весь вызов веб-службы с использованием SOAP.
Есть ли объяснение этому? Является ли XML (или реализация SOAP SOF) большим преимуществом в этой области или есть какие-либо вещи, которые я могу изменить на стороне клиента (я бы скорее не изменил сервис, но изменение DTO на стороне клиента было приемлемым), чтобы попытаться повысить производительность? Похоже, что я уже выбрал некоторые настройки на стороне JSON.net, которые должны сделать это быстрее, чем настройки по умолчанию, нет? Что, по-видимому, является узким местом здесь?
Ответы
Ответ 1
Я потратил немного больше времени на чтение внутренних деталей JSON.NET, и я пришел к выводу, что медлительность вызвана главным образом отражением.
На сайте JSON.NET я нашел несколько хороших советов по производительности, и я пробовал практически все (JObject.Parse, Custom Converters и т.д.), но я не мог 't сжимают любое значительное улучшение производительности. Затем я прочитал самую важную заметку на всем сайте:
Если производительность важна, и вы не против больше кода, чтобы получить ее, это ваш лучший выбор. Подробнее об использовании JsonReader/JsonWriter здесь
Итак, я слушал совет, и я реализовал базовую версию JsonReader для эффективного чтения строки:
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
}
}
Я думаю, что упражнение ясное, и , без сомнения, это лучшая производительность, которую вы можете получить из JSON.NET.
Только этот ограниченный код в 12 раз быстрее, чем версия Deserialize
на моем ящике с 500 номерами, но, конечно, сопоставление не завершено. Тем не менее, я уверен, что он будет как минимум в 5 раз быстрее десериализации в худшем случае.
Ознакомьтесь с этой ссылкой для получения дополнительной информации о JsonReader и о том, как ее использовать:
http://james.newtonking.com/json/help/html/ReadingWritingJSON.htm
Ответ 2
Теперь я использовал предложения как ZenCoder, так и мифов, и я провел больше тестов. Я также заметил ошибку в моей первой тестовой настройке, потому что, пока я создавал инструмент в режиме Release, я все еще запускал тестовое приложение из Visual Studio, которое все еще добавляло некоторые отладочные издержки, и это значительно увеличило разницу в JSON.Net по сравнению с XML-стороной SOAP на моем ПК, поэтому разница в практическом опыте исходных результатов тестирования была уже немного меньше.
В любом случае ниже приведены результаты сбора 5000/50000 номеров с сервера (localhost), включая их сопоставление с моделями.
5000 номеров:
----- 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 номеров:
----- 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
Условные обозначения:
- JSON.Net(отражение) → JsonConvert.DeserializeObject(тот же код JSON.Net, что и выше)
- ServiceStack (отражение) → JsonSerializer.DeserializeFromString
- SOAP Xml (ручное сопоставление) → Тот же клиентский вызов SOAP, как указано выше, с добавлением сопоставления из DTO в модели
-
JSON.Net(ручное сопоставление) → Сопоставление JSON с моделями напрямую с использованием кода на основе кода ZenCoder выше, расширенного для включения отображения для всего запроса (комнаты и местоположения также )
-
ServiceStack (ручное сопоставление) → См. приведенный ниже код (на примере: 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"),
}),
}),
});
Примечания/личные выводы:
- Даже десериализация, основанная на отражении, не намного медленнее, чем создание объекта SOAP XML в режиме фактической версии (oops)
- Ручное сопоставление в JSON.Net быстрее, чем автоматическое сопоставление, и оно очень сопоставимо по скорости с производительностью отображения SOAP Xml, и оно предлагает большую свободу, что отлично, особенно когда модели и DTO отличаются местами
- Ручное сопоставление ServiceStack на самом деле медленнее, чем их полное отражение. Я предполагаю, что это связано с тем, что это сопоставление вручную более высокого уровня, чем на стороне JSON.Net, потому что, похоже, уже возникло какое-то генерация объектов. Возможно, есть и альтернативы более низкого уровня на стороне ServiceStack?
- Все это было сделано с помощью кода сервера/клиента, работающего на одном компьютере. В отдельных производственных средах клиент/сервер я уверен, что решения JSON должны бить SOAP XML из-за гораздо меньших сообщений, которые необходимо отправлять по сети.
- В этой ситуации автосохранение JSON.Net выглядит чуть медленнее, чем ServiceStack для больших ответов.
Ответ 3
var receivedObject = JsonConvert.DeserializeObject<dynamic>(content);
работает намного быстрее для меня:
var receivedObject = JsonConvert.DeserializeObject<Product>(content);
и это еще быстрее:
dynamic receivedObject = JObject.Parse(content); // The same goes for JArray.Parse()
Ответ 4
Здесь я добавляю еще 2 пункта, которые помогают мне повысить производительность моего IoT-приложения. Я получал миллионы сообщений JSON каждый день.
- Изменения в экземпляре ContractResolver
Старый код
return JsonConvert.SerializeObject(this, Formatting.Indented,
new JsonSerializerSettings
{
ContractResolver = AppConfiguration.CamelCaseResolver
});
Новый код
return JsonConvert.SerializeObject(this, Formatting.Indented,
new JsonSerializerSettings
{
ContractResolver = AppConfiguration.CamelCaseResolver
});
- Избегайте создания JObject
Старый код
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
Чтобы проверить преимущества в производительности, я использовал benchmarkdotnet, чтобы увидеть разницу. проверьте и эту ссылку.