Мы должны помнить о производительности при написании кода, избегая при этом нано-оптимизаций. Иногда то, что выглядит как скромная оптимизация, имеет огромное значение. Сегодня мы увидим, как System.Text.Json имеет ловушку снижения производительности.
Что не так с этим кодом?
var options = new JsonSerializerOptions { WriteIndented = true }; | |
string jsonString = JsonSerializer.Serialize(myObject, options); |
Каждый раз, когда мы запускаем эту функцию, она создает новый JsonSerializerOptions. Насколько плохо каждый раз создавать экземпляр нового объекта? Как правило, создание нового объекта каждый раз незначительно с точки зрения производительности и распределения памяти. Тем не менее, выполняя профилирование производительности, моя команда получила подозрительные результаты. Время, затраченное на эти функции, было равно:
- ОтправитьToServiceBus()
- Сериализация()
Это вызывает опасения: во-первых, это сериализация данных, для которых наши пакеты хорошо оптимизированы, а во-вторых, передача данных по сети, где ожидается задержка.
Почему сериализация такая медленная?
Новый System.Text.Json использует ссылку экземпляра JsonSerializerOptions для кэширования метаданных сериализации, поэтому, если вы предоставляете новый экземпляр каждый раз, вы теряете кэширование, и код должен выполнять дорогостоящую операцию создания метаданных для каждой функции. вызов.
Проблема настолько распространена, что Microsoft даже посвятила ей целую страницу.
Пример
Документы предоставляют этот эталонный пример:
using System.Diagnostics; | |
using System.Text.Json; | |
namespace OptionsPerfDemo | |
{ | |
public record Forecast(DateTime Date, int TemperatureC, string Summary); | |
public class Program | |
{ | |
public static void Main() | |
{ | |
Forecast forecast = new(DateTime.Now, 40, "Hot"); | |
JsonSerializerOptions options = new() { WriteIndented = true }; | |
int iterations = 100000; | |
var watch = Stopwatch.StartNew(); | |
for (int i = 0; i < iterations; i++) | |
{ | |
Serialize(forecast, options); | |
} | |
watch.Stop(); | |
Console.WriteLine($"Elapsed time using one options instance: {watch.ElapsedMilliseconds}"); | |
watch = Stopwatch.StartNew(); | |
for (int i = 0; i < iterations; i++) | |
{ | |
Serialize(forecast); | |
} | |
watch.Stop(); | |
Console.WriteLine($"Elapsed time creating new options instances: {watch.ElapsedMilliseconds}"); | |
} | |
private static void Serialize(Forecast forecast, JsonSerializerOptions? options = null) | |
{ | |
_ = JsonSerializer.Serialize<Forecast>( | |
forecast, | |
options ?? new JsonSerializerOptions() { WriteIndented = true }); | |
} | |
} | |
} | |
// Produces output like the following example: | |
// | |
//Elapsed time using one options instance: 190 ms | |
//Elapsed time creating new options instances: 40140 ms |
Как вы можете видеть из документов, разница огромна, более чем в 200 раз дороже. И ничто не скажет вам об этом, ни исключения, ни предупреждения. Единственный способ найти это — во время нагрузочного тестирования и профилирования. Это заставляет меня задаться вопросом, должно ли это быть предупреждением, поэтому мы уменьшаем количество раз, когда эта ошибка будет сделана.
Заключение
Хорошие практики, такие как знание жизненных циклов объектов и оптимизация создания синглетонов, могут окупиться больше, чем то, на что они похожи. Поэтому я рекомендую учиться, когда вам нужен синглтон или новый объект для каждого запроса.
Помните, что создание параметров сериализатора каждый раз имеет серьезные последствия, поскольку инфраструктура кэширует внутренние метаданные по ссылке SerializerOptions.
Если вы видите новый JsonSerializerOptions(), убедитесь, что он относится к шаблону Singleton.
Удачного кодирования.