在.NET 6.0中,我们正在运送一个新的C#源生成器,以帮助提高使用System.Text.Json
.NET的应用程序的性能。在这篇文章中,我将介绍我们为什么要建立它,它是如何工作的,以及你可以在你的应用程序中体验到什么好处。
随着System.Text.Json
源生成器的引入,我们现在有几种在.NET中进行JSON序列化的模式可供选择,使用JsonSerializer
。现有的模式是由运行时反射支持的,还有两种新的编译时源码生成模式;其中生成器生成优化的序列化逻辑,静态数据访问模型,或者两者都是。在这两种源码生成方案中,生成的工件被直接传递给JsonSerializer
,作为性能优化。下面是每个序列化模型所提供的功能的概述。
JsonSerializer | JsonSerializer + 预生成优化的序列化逻辑 | JsonSerializer + 预生成数据访问模型 | |
---|---|---|---|
增加序列化的吞吐量 | 不(基线) | 有 | 没有 |
减少启动时间 | 没有(基线) | 是 | 是 |
减少私人内存的使用 | 没有(基线) | 是 | 是 |
消除运行时反射 | 没有 | 是 | 是 |
有助于以安全方式减少应用程序的大小 | 不 | 是的 | 是 |
支持所有的序列化功能 | 是 | 不支持 | 是的 |
获取源码生成器
源码生成器可用于任何.NET C#项目,包括控制台应用程序、类库、网络和Blazor应用程序。你可以通过使用System.Text.Json NuGet软件包的最新构建来试用源代码生成器。从即将推出的.NET 6.0预览版7开始,在针对net6.0
。
除了.NET 6.0之外,该源码生成器还兼容其他目标框架名称(TFM),即.NET 5.0及以下版本、.NET框架和.NET标准。生成的源代码的API形状在不同的TFM中是一致的,但根据每个TFM上可用的框架API,其实现可能会有所不同。
C#源代码生成所需的.NET SDK的最小版本是.NET 5.0。
什么是System.Text.Json
源码生成器?
几乎所有的.NET序列化器的骨干是反射。反射为某些场景提供了很好的能力,但不能作为高性能云原生应用程序的基础(通常是(去)序列化和处理大量的JSON文档)。反射对于启动、内存使用和汇编修剪都是一个问题。
运行时反射的一个替代方案是编译时源码生成。源码生成器生成的C#源代码文件可以作为库或应用程序构建的一部分进行编译。在编译时生成源代码可以为.NET应用程序提供许多好处,包括提高性能。在.NET 6中,我们包括一个新的源码生成器,作为System.Text.Json
的一部分。JSON源码生成器与JsonSerializer
一起工作,并可以以多种方式进行配置。现有的JsonSerializer
功能将继续按原样工作,所以你可以决定是否使用新的源生成器。JSON源码生成器为提供这些好处所采取的方法是将JSON可序列化类型的运行时检查转移到编译时,在那里它生成一个静态模型来访问类型上的数据,直接使用Utf8JsonWriter
,或同时使用优化的序列化逻辑。
为了选择源码生成,你可以定义一个部分类,它派生自一个新的类,叫做JsonSerializerContext
,并使用一个新的JsonSerializableAttribute
类来表示可序列化的类型。
例如,给定一个简单的Person
类型来进行序列化:
namespace Test
{
internal class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
复制代码
我们会向源码生成器指定该类型,如下所示:
using System.Text.Json.Serialization;
namespace Test
{
[JsonSerializable(typeof(Person))]
internal partial class MyJsonContext : JsonSerializerContext
{
}
}
复制代码
作为构建的一部分,源码生成器将用以下形状来增强MyJsonContext
部分类:
internal partial class MyJsonContext : JsonSerializerContext
{
public static MyJsonContext Default { get; }
public JsonTypeInfo<Person> Person { get; }
public MyJsonContext(JsonSerializerOptions options) { }
public override JsonTypeInfo GetTypeInfo(Type type) => ...;
}
复制代码
生成的源代码可以通过直接传递给JsonSerializer
上的新重载而被集成到编译的应用程序中。
Person person = new() { FirstName = "Jane", LastName = "Doe" };
byte[] utf8Json = JsonSerializer.SerializeToUtf8Bytes(person, MyJsonContext.Default.Person);
person = JsonSerializer.Deserialize(utf8Json, MyJsonContext.Default.Person):
复制代码
为什么要做源码生成器?
JsonSerializer
是一个将.NET对象序列化为JSON字符串的工具,并从JSON字符串中反序列化.NET对象。为了处理一个类型,序列化器需要关于如何访问其成员的信息。在序列化时,序列化器需要访问对象的属性和字段获取器。同样地,当反序列化时,序列化器需要访问构造器来实例化该类型,也需要访问该对象的属性和字段的设置器。
System.Text.Json
在使用 ,通过 (允许运行时配置),以及通过 和 (允许设计时配置)等属性,暴露了影响序列化和反序列化行为的机制。当对一个类型的实例进行序列化和反序列化时,序列化器需要关于这个配置的信息,以便它能够被尊重。JsonSerializer
JsonSerializerOptions
[JsonPropertyName(string)]
[JsonIgnore]
当处理JSON可序列化的类型时,序列化器需要关于对象成员和特征配置的结构化、优化的格式的信息。我们可以把这种结构化信息称为序列化类型所需的序列化元数据。
在以前的版本System.Text.Json
,序列化元数据只能在运行时计算,在传递给序列化器的任何对象图中的每个类型的第一个序列化或反序列化例程中进行。在这个元数据生成后,序列化器会执行实际的序列化和反序列化。这个计算的结果被缓存起来,以便在随后的JSON处理例程中重复使用。生成阶段是基于反射的,在时间和分配上都很昂贵。我们可以把这个阶段称为序列化器的 "热身 "阶段。
System.Text.Json
源码生成器通过将使用反射的可序列化类型的运行时检查转移到编译时来帮助我们消除这个预热阶段。这种检查的结果可以是初始化结构化序列化元数据实例的源代码。生成器还可以生成高度优化的序列化逻辑,它可以尊重提前指定的一组序列化特性。默认情况下,生成器会发出这两种源,但是可以配置为只生成其中一种输出,或者跨一组类型,或者每个可序列化类型。
这个生成的元数据被包含在编译的程序集中,在那里它可以被初始化并直接传递给JsonSerializer
,这样序列化器就不必在运行时生成它。这有助于减少每个类型的第一次序列化或反序列化的成本。有了这些特性,使用源码生成器可以为使用System.Text.Json
的应用程序提供以下好处:
- 增加序列化的吞吐量
- 减少了启动时间
- 减少了私有内存的使用
- 移除运行时使用的
System.Reflection
和System.Reflection.Emit
- 减少应用程序大小的Trim-compatible序列化
引入JsonTypeInfo<T>
,JsonTypeInfo
, 和JsonSerializerContext
JsonTypeInfo<T>
,JsonTypeInfo
, 和JsonSerializerContext
类型的实现是JSON源生成的主要结果。
JsonTypeInfo<T>
类型包含关于如何序列化和反序列化单一类型的结构化信息。这些信息可以包含关于如何访问其成员的元数据。当序列化器本身进行类型的(反)序列化时,需要这些信息,使用它所具有的强大逻辑来支持所有可以用JsonSerializerOptions
或序列化属性配置的功能。这包括先进的功能,如异步(去)序列化和引用处理。当只需要一组有限的功能时,JsonTypeInfo<T>
可以包含优化的、预先生成的序列化逻辑(直接使用Utf8JsonWriter
),序列化器可以调用它,而不是通过自己的代码路径。调用这种逻辑可以使序列化的吞吐量大幅提高。一个JsonTypeInfo<T>
实例被紧密地绑定在一个JsonSerializerOptions
的实例上。
JsonTypeInfo
类型为JsonTypeInfo<T>
提供了一个无类型的抽象。JsonSerializer
利用它从JsonSerializerContext
实例中通过JsonSerializerContext.GetTypeInfo
检索一个JsonTypeInfo<T>
实例。
JsonSerializerContext
类型包含多个类型的JsonTypeInfo<T>
实例。除了帮助JsonSerializer
通过JsonSerializerContext.GetTypeInfo
检索JsonTypeInfo<T>
的实例外,它还提供了一种机制,使用用户提供的特定JsonSerializerOptions
实例来初始化所有的类型元数据。
源生成模式
System.Text.Json
源生成器有两种模式:一种是生成类型元数据初始化逻辑,另一种是生成序列化逻辑。用户可以根据(去)序列化的情况,将源码生成器配置为对项目中可序列化的JSON类型使用这两种模式中的一种或两种。为一个类型生成的元数据包含结构化的信息,其格式可以被序列化器优化利用,将该类型的实例序列化和反序列化为JSON表示。序列化逻辑使用对Utf8JsonWriter
方法的直接调用来编写.NET对象的JSON表示,使用一套预先确定的序列化选项。默认情况下,源码生成器同时生成元数据初始化逻辑和序列化逻辑,但可以配置为只生成一种逻辑。要为整个上下文(可序列化类型的集合)设置生成模式,请使用JsonSourceGenerationOptionsAttribute.GenerationMode
,而要为特定类型设置模式,请使用JsonSerializableAttribute.GenerationMode
。
生成优化的序列化逻辑
我们要看的第一个源码生成模式是JsonSourceGenerationMode.Serialization
。它通过生成直接使用Utf8JsonWriter
的源码,提供了比使用现有JsonSerializer
方法高得多的性能。简而言之,源码生成器提供了一种方法,在编译时给你一个不同的实现,以便使运行时的体验更好。
JsonSerializer
是一个强大的工具,它有许多功能,可以影响.NET类型从/到JSON格式的(去)序列化。它的速度很快,但当一个序列化程序只需要一个子集的功能时,会有一些性能开销。展望未来,我们将一起更新 和新的源码生成器。有时,一个新的 功能会伴随着对优化的序列化逻辑的支持,有时则没有,这取决于生成支持该功能的逻辑的可行性如何。JsonSerializer
JsonSerializer
鉴于我们上面的Person
类型,源码生成器可以被配置为为该类型的实例生成序列化逻辑,并给出一些预定义的序列化选项。注意,MyJsonContext
这个类名是任意的。你可以使用任何你想要的类名:
using System.Text.Json.Serialization;
namespace Test
{
[JsonSerializerOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(Person))]
internal partial class MyJsonContext : JsonSerializerContext
{
}
}
复制代码
我们已经定义了一组JsonSerializer 的特性,在这种模式下通过JsonSourceGenerationOptionsAttribute
。如上所示,这些特性可以提前指定给源码生成器,以避免运行时的额外检查。
作为构建的一部分,源码生成器用上面显示的相同的形状增强了MyJsonContext
部分类。
采用这种模式的序列化器调用可能看起来像下面的例子。这个例子允许我们删去很多System.Text.Json
的实现,因为我们没有调用到JsonSerializer
:
using MemoryStream ms = new();
using Utf8JsonWriter writer = new(ms);
MyJsonContext.Default.Person.Serialize(writer, new Person { FirstName = "Jane", LastName = "Doe" });
writer.Flush();
// Writer contains:
// {"firstName":"Jane","lastName":"Doe"}
复制代码
另外,你可以继续使用JsonSerializer
,并通过MyJsonContext.Default.Person
,将生成的代码的实例传递给它。
JsonSerializer.Serialize(person, MyJsonContext.Default.Person);
复制代码
下面是一个类似的用法,有一个不同的重载:
JsonSerializer.Serialize(person, typeof(Person), MyJsonContext.Default);
复制代码
这两个重载的区别在于,第一个重载使用的是类型化的元数据实现--JsonTypeInfo<T>
,而第二个重载使用的是一个更通用的JsonSerializerContext
实现,它进行类型测试以确定是否存在类型化的实现。因此,它的速度有点慢(由于类型测试)。如果对于一个给定的类型没有源生成的实现,那么序列化器会抛出一个NotSupportedException
。它不会回退到基于反射的实现(作为一个明确的设计选择)。
在上面的例子中,MyJsonContext.Default.Person
属性返回一个JsonTypeInfo<Person>
。Default
属性返回一个MyJsonContext
实例,其后援JsonSerializerOptions
实例与JsonSourceGenerationOptionsAttribute
在JsonSerializerContext
上设置的值相匹配。如果该属性不存在,将使用一个默认的JsonSerializerOptions
实例(即new JsonSerializerOptions(JsonSerializerDefaults.General)
的结果)。
这种最快和最优化的源生成模式--基于Utf8JsonWriter
--目前只适用于序列化。对反序列化的类似支持--基于Utf8JsonReader
--将被考虑在未来的.NET版本中予以支持。你将在下面的章节中看到,还有其他有利于反序列化的模式。
生成类型元数据初始化逻辑
生成器可以被配置为生成类型元数据初始化逻辑--用JsonSourceGenerationMode.Metadata
模式--而不是完整的序列化逻辑。这种模式为执行序列化和反序列化逻辑时要调用的常规JsonSerializer
代码路径提供了一个静态数据访问模型。如果你需要JsonSourceGenerationMode.Serialization
模式不支持的功能,例如引用处理和异步序列化,这种模式就很有用。这种模式也为反序列化提供了好处,而序列化逻辑模式则没有。
JsonSourceGenerationMode.Metadata
模式提供了源生成的大部分好处,除了改进序列化吞吐量。在这种模式下,运行时元数据的生成被转移到了编译时。像之前的模式一样,所需的元数据被生成到编译程序集中,在那里它可以被初始化并传递给JsonSerializer
。这种方法减少了每种类型的第一次序列化或反序列化的成本。前面说的其他好处大部分也适用,比如更低的内存使用率。
这种模式的配置方式与前面的例子类似,只是我们没有提前指定功能选项,而是指定了不同的生成模式。
using System.Text.Json.Serialization;
namespace Test
{
[JsonSerializable(typeof(Person), GenerationMode = JsonSourceGenerationMode.Metadata)]
internal partial class MyJsonContext : JsonSerializerContext
{
}
}
复制代码
生成器用与前面所示相同的形状增强部分上下文类。
采用这种模式的序列化器调用会像下面的例子一样:
JsonSerializerOptions options = new()
{
ReferenceHander = ReferenceHandler.Preserve,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// Use your custom options to initialize a context instance.
MyJsonContext context = new(options);
string json = JsonSerializer.Serialize(person, context.Person);
// {"id":1,"firstName":"Jane","lastName":"Doe"}
复制代码
使用这种模式,你应该看到性能的显著提高,同时享受(去)序列化器的全部功能。根据你的需要,这种模式是一个不错的中间选择。
同时生成序列化逻辑和元数据初始化逻辑
源生成模式的默认配置是JsonSourceGenerationMode.Default
。这是一个 "一切都在 "的模式,启用了刚才所涉及的两种源生成器模式。例如,你可能只需要与JsonSourceGenerationMode.Serialization
兼容的序列化功能,但也想提高反序列化性能。在这种情况下,你应该看到更快的初始序列化和反序列化,更快的序列化吞吐量,更低的内存使用,以及通过汇编修剪减少应用程序的大小。
你要按以下方式配置生成器。请注意,你不必指定生成模式,因为默认是生成两种逻辑:
using System.Text.Json.Serialization;
namespace Test
{
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Person))]
internal partial class MyJsonContext : JsonSerializerContext
{
}
}
复制代码
同样,生成的API形状也保持不变。我们可以按如下方式使用生成的源:
// Serializer invokes pre-generated serialization logic for increased throughput and other benefits.
string json = JsonSerializer.Serialize(person, MyJsonContext.Default.Person);
// Serializer uses pre-generated type-metadata and avoids warm-up stage for deserialization, alongside other benefits.
Person person = JsonSerializer.Deserialize(json, MyJsonContext.Default.Person);
复制代码
JsonSerializer
上的新API和在System.Net.Http.Json
除了我们在配置源码生成器时经历的新API,我们还增加了消费它的输出的API。我们已经在上面的例子中看到了一些新的API,我们把JsonTypeInfo<T>
和JsonSerializerContext
实例直接传递给JsonSerializer
,作为一种性能优化。我们还为JsonSerializer
增加了更多的重载,也在System.Net.Http.Json
APIs中增加了重载,这有助于在与HttpClient
和JsonContent
交互时优化对JSON数据的处理。
新的JsonSerializer
APIs
namespace System.Text.Json
{
public static class JsonSerializer
{
public static object? Deserialize(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerContext context) => ...;
public static object? Deserialize(ReadOnlySpan<char> json, Type returnType, JsonSerializerContext context) => ...;
public static object? Deserialize(string json, Type returnType, JsonSerializerContext context) => ...;
public static object? Deserialize(ref Utf8JsonReader reader, Type returnType, JsonSerializerContext context) => ...;
public static ValueTask<object?> DeserializeAsync(Stream utf8Json, Type returnType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static TValue? Deserialize<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
public static TValue? Deserialize<TValue>(string json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
public static TValue? Deserialize<TValue>(ReadOnlySpan<char> json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
public static TValue? Deserialize<TValue>(ref Utf8JsonReader reader, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
public static string Serialize(object? value, Type inputType, JsonSerializerContext context) => ...;
public static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerContext context) { }
public static Task SerializeAsync(Stream utf8Json, object? value, Type inputType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task SerializeAsync<TValue>(Stream utf8Json, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static byte[] SerializeToUtf8Bytes(object? value, Type inputType, JsonSerializerContext context) => ...;
public static byte[] SerializeToUtf8Bytes<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
public static void Serialize<TValue>(Utf8JsonWriter writer, TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { }
public static string Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
}
}
复制代码
新的System.Net.Http.Json
APIs
namespace System.Net.Http.Json
{
public static partial class HttpClientJsonExtensions
{
public static Task<object?> GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task<object?> GetFromJsonAsync(this HttpClient client, System.Uri? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
}
public static partial class HttpContentJsonExtensions
{
public static Task<object?> ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
public static Task<T?> ReadFromJsonAsync<T>(this HttpContent content, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
}
public sealed partial class JsonContent : HttpContent
{
public static JsonContent<object?> Create(object? inputValue, Type inputType, JsonSerializerContext context, MediaTypeHeaderValue? mediaType = null) => ...;
public static JsonContent<TValue> Create<TValue>(TValue? inputValue, JsonTypeInfo<TValue> jsonTypeInfo, MediaTypeHeaderValue? mediaType = null) => ...;
}
public sealed partial class JsonContent<TValue> : JsonContent
{
public TValue? Value { get }
}
}
复制代码
源生成如何提供好处
改善序列化吞吐量
预先生成并使用优化的序列化逻辑,只尊重应用程序中需要的功能,这比使用JsonSerializer
's robust serialization logic的性能要高。序列化器支持所有的功能,这意味着在序列化过程中会有更多的逻辑需要撕裂,这在测量过程中会显示出来。
序列化POCOs
鉴于我们的Person
类型,我们可以观察到,当使用源码生成器时,序列化的 速度是 ~1.62倍。
方法 | 平均值 | 误差 | StdDev | 比值 | 比率SD | 0代 | 第1代 | 第2代 | 已分配 |
---|---|---|---|---|---|---|---|---|---|
Serializer | 243.1 ns | 4.83 ns | 9.54 ns | 1.00 | 0.00 | – | – | – | – |
SrcGenSerializer | 149.3 ns | 2.04 ns | 1.91 ns | 0.62 | 0.03 | – | – | – | – |
序列化集合
使用相同的Person
类型,我们观察到在序列化不同长度的数组时,在完全不分配的情况下,性能会有明显的提升。
方法 | 数值 | 平均值 | 误差 | StdDev | 比值 | 0代 | 第1代 | 第2代 | 已分配 |
---|---|---|---|---|---|---|---|---|---|
Serializer | 10 | 2,392.5 ns | 17.42 ns | 13.60 ns | 1.00 | 0.0801 | – | – | 344 B |
SrcGenSerializer | 10 | 989.4 ns | 6.74 ns | 5.62 ns | 0.41 | – | – | – | – |
Serializer | 100 | 21,427.1 ns | 189.33 ns | 167.84 ns | 1.00 | 0.0610 | – | – | 344 B |
SrcGenSerializer | 100 | 10,137.3 ns | 125.61 ns | 111.35 ns | 0.47 | – | – | – | – |
Serializer | 1000 | 215,102.4 ns | 1,737.91 ns | 1,356.85 ns | 1.00 | – | – | – | 344 B |
SrcGenSerializer | 1000 | 104,970.5 ns | 345.48 ns | 288.49 ns | 0.49 | – | – | – | – |
TechEmpower 缓存基准
TechEmpower缓存基准检验了一个平台或框架对来自数据库的信息的内存缓存。按照基准规范,我们对缓存的数据进行JSON序列化,以便将它们作为响应发送到测试线束。通过源码生成,我们已经能够大幅提高我们在这个基准中的性能。
请求/秒 | 请求数 | |
---|---|---|
net5.0 | 243,000 | 3,669,151 |
net6.0 | 260,928 | 3,939,804 |
net6.0 + JSON source gen | 364,224 | 5,499,468 |
我们观察到约100K的RPS增益,这是一个约40%的增长。
更快的启动和减少私有内存
将类型元数据的检索从运行时转移到编译时,意味着序列化器在启动时要做的工作更少,这导致了执行每个类型的第一次序列化或反序列化的时间减少。
在以前的版本System.Text.Json
,序列化器总是尽可能地使用Reflection.Emit
,以生成构造函数、属性和字段的快速成员访问器。生成这些IL方法需要花费非同小可的时间,但也会消耗私有内存。通过源代码生成器,我们能够生成静态调用这些访问器的代码。这就消除了反射所带来的时间和分配成本。
同样地,所有JSON数据的序列化和反序列化都是在JsonConverter<T>
实例中进行的。序列化器将静态地初始化几个内置的转换器实例以提供默认功能。用户应用程序支付了这些分配的费用,即使在输入对象图的情况下只需要几个这样的转换器。通过源码生成,我们可以只初始化向生成器指示的类型所需要的转换器(在JsonSourceGeneration.Metadata
模式下),也可以完全跳过转换器的使用(在JsonSourceGeneration.Serialization
模式下)。
使用一个简单的Message
类型:
public class JsonMessage
{
public string message { get; set; }
}
复制代码
我们可以观察到序列化和反序列化过程中的启动改进。
串行化
经过的时间(ms) | 已分配(KB) | |
---|---|---|
序列化器 | 28.25 | 1110.00 |
SrcGenSerializer | 12.75 | 563.00 |
反序列化
耗时(ms) | 已分配(KB) | |
---|---|---|
Serializer | 28.25 | 1110.00 |
SrcGenSerializer | 12.75 | 563.00 |
我们看到,对该类型进行第一次(去)序列化所需的时间和分配量都大大减少。
修剪的正确性
通过消除运行时反射,我们避免了对ILLinker分析不友好的主要编码模式。鉴于基于反射的代码被修剪掉了,使用System.Text.Json
的应用程序在修剪时从有几个ILLinker分析警告变成完全没有。这意味着使用System.Text.Json
源码生成器的应用程序可以安全地进行修剪,只要用户应用程序本身或BCL的其他部分没有其他警告。
减少应用程序的大小
通过在编译时而不是在运行时检查可序列化的类型,主要做了两件事来减少消费应用程序的大小。首先,我们可以在运行时检测应用程序中需要哪些自定义或内置的JsonConverter<T>
类型,并在生成的元数据中静态地引用它们。这使得ILLinker可以修剪掉应用程序在运行时不需要的JSON转换器类型。同样地,在编译时检查输入类型就不需要在运行时检查了。这就消除了在运行时对大量System.Reflection
APIs的需求,因此ILLinker可以在System.Text.Json
,修剪与这些APIs一起使用的内部代码。在依赖关系图中未使用的源代码也被修剪掉了。
大小的削减是基于哪些JsonSerializer
方法被使用。如果使用了预先生成的元数据的新API,那么你可能会在你的应用程序中观察到大小的减少。
使用一个简单的控制台程序,使用JSON源生成对我们的Message
类型进行往返序列化和反序列化,与不使用源生成的同一程序相比,我们可以观察到修剪后的大小减少。
程序大小(MB) | System.Text.Json.dll大小(KB) | |
---|---|---|
Serializer | 21.7 | 989 |
SrcGenSerializer | 20.8 | 460 |
在ASP.NET核心中使用源码生成的代码
Blazor
在Blazor应用程序中,可序列化类型的预生成逻辑可以通过正在添加到System.Net.Http.Json
名称空间中的新API直接转发给序列化器。例如,要从HttpClient
中异步反序列化天气预报对象的列表,可以使用HttpClient.GetFromJsonAsync
方法的新重载。
[JsonSerializable(typeof(WeatherForecast[]))]
internal partial class MyJsonContext : JsonSerializerContext { }
@code {
private WeatherForecast[] forecasts;
private static JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
private static MyJsonContext Context = new MyJsonContext(Options);
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync("sample-data/weather.json", Context.WeatherForecastArray);
}
}
复制代码
MVC, WebAPIs, SignalR, Houdini
这些ASP.NET Core服务可以通过已经存在的API检索用户生成的上下文来配置序列化选项。
[JsonSerializable(typeof(WeatherForecast[]))]
internal partial class MyJsonContext : JsonSerializerContext { }
services.AddControllers().AddJsonOptions(options => options.AddContext<MyJsonContext>());
复制代码
在未来,这些服务可以暴露出API,直接采取JsonTypeInfo<T>
或JsonSerializerContext
实例。
代表他人执行JSON序列化
与上述ASP.NET Core场景类似,库或框架开发者可以提供新的API,接受JsonSerializerOptions
或JsonSerializerContext
实例,代表用户转发给序列化器。
版本管理
你可能想知道为什么源生成器的输出要通过新的重载直接传递给JsonSerializer
,而不是说在应用程序启动时隐式初始化,并在全局缓存中注册,以便序列化器能够检索到它。一个好处是对应用程序进行修剪。通过为源码生成的序列化器和数据访问模型提供专门的入口,我们为序列化器静态地提供了序列化输入类型所需的所有信息,这使得它可以摆脱大量未使用的代码和依赖关系(包括与反射相关的和其他的)。另一个好处是,它为调用者提供了一个考虑版本的机会,即为可序列化类型生成的代码是否与可序列化类型本身同步。这是使用源码生成时的一个重要考虑因素,特别是对于序列化来说。
如果源码生成器的输出与相应的可序列化类型不匹配,会导致严重的问题,包括数据被意外地包含或排除在序列化之外。这些问题可能真的很难诊断。源码生成器的设计避免了可能导致版本问题的模式,如对生成的工件进行应用程序全局注册。这种全局注册会导致不同程序集的序列化设置之间的争论问题。每个程序集可能使用不同版本的源码生成器,可能导致应用程序在开发和生产中具有不同的行为,这取决于序列化器选择哪一组生成的工件(在不同程序集中的实现)。
结束
提高使用System.Text.Json
的应用程序的性能是一个持续的过程,也是该库成立以来的一个主要目标。编译时的源码生成帮助我们提高了性能,并开发了一个新的修剪安全的序列化模型。我们希望你能尝试一下源码生成器,并给我们提供反馈,告诉我们它对你的应用程序中的香水有何影响,任何可用性问题,以及你可能发现的任何错误。
我们永远欢迎社区的贡献。如果你想为System.Text.Json
,请查看我们在GitHub上的up-for-grabs 问题列表。
作者:后端之巅
链接:https://juejin.cn/post/7131295934989729799
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。