.Net内System.Text.Json组件在序列化的性能方面已经非常出色了,如果你想追求更极端的JSON处理性能那下面所分享的内容对你来说应该是会有帮助的。接下来但要一下普通和极端的使用方式。
MemoryStream stream = new MemoryStream();
World wd = new World();
JsonSerializer.Serialize(stream, wd);
var bytes = stream.GetBuffer();
var arraySegment = new ArraySegment<byte>(bytes, 0, (int)stream.Length);
//var bytes=stream.ToArray();
如果你想把一个对象序化成Byte[]对象,相信大部分人都这样写就完事了。然后再针对MemoryStream建个Pool或[ThreadState]那似已经很完美了!
其实Serialize每次调用都会创建Utf8JsonWriter对象,而真正工作的即是这个对象,如果你想节省这个对象可以创建这样一个对象并复用它
public class JsonExten
{
public static Utf8JsonWriter GetJsonWriter(Stream stream)
{
Utf8JsonWriter utf8JsonWriter = _utf8JsonWriter ??= new Utf8JsonWriter(stream, new JsonWriterOptions { SkipValidation = true });
utf8JsonWriter.Reset(stream);
return utf8JsonWriter;
}
[ThreadStatic]
private static Utf8JsonWriter _utf8JsonWriter;
}
可以针对Utf8JsonWriter建个Pool,但获取和回收也是有代码损耗的,并没有[ThreaStatic]来得简单实在,其实System.Text.Json设计者已经考虑到这问题所以添加了Reset方法来重置它。接下来就可以复用这个对象来序列化了
MemoryStream stream = new MemoryStream();
World wd = new World();
var writer = JsonExten.GetJsonWriter(stream);
JsonSerializer.Serialize(writer, wd);
这样就能节省每次Utf8JsonWriter的开销,不过这样序列化对象一定会存在反射或JIT过程生成一个额外的开销处理;如果你想节省这一步的损耗就可以更极端的去手动写入每一个成员
MemoryStream stream = new MemoryStream();
World wd = new World();
var writer = JsonExten.GetJsonWriter(stream);
writer.WriteStartObject();
writer.WriteNumber("Id", wd.Id);
writer.WriteNumber("RandomNumber", wd.RandomNumber);
writer.WriteEndObject();
writer.Flush();
以上手动处理每个成员效率是高,但使用起来就不灵活了,只有极个别追求性能而结构类型单一的服务适合使用。
上面介绍了如果更优地使用Utf8JsonWriter,其实以上使用还是有很大优化空间的,主要原因是使用了MemoryStream序列化流的作为载体。即使使用MemoryStream Pool或[ThreaStatic]也无法解决这些内存复制的缺陷!接下来看下几段代码
Utf8JsonWriter基于Stream构建的方法和Reset方法
public Utf8JsonWriter(Stream utf8Json, JsonWriterOptions options = default(JsonWriterOptions))
{
if (utf8Json == null)
{
ThrowHelper.ThrowArgumentNullException("utf8Json");
}
if (!utf8Json.CanWrite)
{
throw new ArgumentException(System.SR.StreamNotWritable);
}
_stream = utf8Json;
SetOptions(options);
_arrayBufferWriter = new ArrayBufferWriter<byte>();
}
public void Reset(Stream utf8Json)
{
CheckNotDisposed();
if (utf8Json == null)
{
throw new ArgumentNullException("utf8Json");
}
if (!utf8Json.CanWrite)
{
throw new ArgumentException(System.SR.StreamNotWritable);
}
_stream = utf8Json;
if (_arrayBufferWriter == null)
{
_arrayBufferWriter = new ArrayBufferWriter<byte>();
}
else
{
_arrayBufferWriter.Clear();
}
_output = null;
ResetHelper();
}
Utf8JsonWriter.Flush的代码
public void Flush()
{
CheckNotDisposed();
_memory = default(Memory<byte>);
if (_stream != null)
{
if (BytesPending != 0)
{
_arrayBufferWriter.Advance(BytesPending);
BytesPending = 0;
_stream.Write(_arrayBufferWriter.WrittenSpan);
BytesCommitted += _arrayBufferWriter.WrittenCount;
_arrayBufferWriter.Clear();
}
_stream.Flush();
}
else if (BytesPending != 0)
{
_output.Advance(BytesPending);
BytesCommitted += BytesPending;
BytesPending = 0;
}
}
Stream的 Write(ReadOnlySpan<byte> buffer)代码
public virtual void Write(ReadOnlySpan<byte> buffer)
{
byte[] sharedBuffer = ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
buffer.CopyTo(sharedBuffer);
Write(sharedBuffer, 0, buffer.Length);
}
finally
{
ArrayPool<byte>.Shared.Return(sharedBuffer);
}
}
细心的你有没发现问题?Utf8JsonWriter并不会直接把内容写入到Stream中,而是先写入到_arrayBufferWriter中,然后再提交的时候才写入Stream这是第一次复制。然而更让你无语的是Write(ReadOnlySpan<byte> buffer)方法,由于ReadOnlySpan<byte>是不能获取到引用Byte[]的...所以只能是先复制到一个临时的缓冲区后再调用Write(byte[] buffer, int offset, int count)写入!看上去是不是有点脱库子放屁?
通过以上分析想更好性能地使用Utf8JsonWriter应该基于IBufferWriter<byte>的构造函数创建它,IBufferWriter<byte>的默认实现是ArrayBufferWriter<byte>,它是一个自动扩容的缓冲区,比起Stream功能就简单多了。如果想达到更好的性能还是和Pool结合使用。
BeetleX为了更好的解决多次复制问题也重写了Steam的方法,直接把内容写到网络游的缓冲区中
public override void Write(ReadOnlySpan<byte> buffer)
{
int count = buffer.Length;
while (count > 0)
{
var memory = WriteSocketStream.GetWriteSpan(count);
buffer.Slice(0, memory.Length).CopyTo(memory);
WriteAdvance(memory.Length);
buffer = buffer.Slice(memory.Length);
count -= memory.Length;
}
}
然而这只能节省一次的复制,最好的办法就还是基于Stream实现IBufferWriter<byte>,这样就能直接序列化到网络流中一次复制环节也不存在了。
总的来说以后各家的序列化组件都应该会兼容IBufferWriter<byte>的了,毕竟这种可以预分配后更新的结构在很多时候有着很大的性能优势。Stream这种一旦写入就无法更改之前写入的个别区域,很多时候为了计算长度不得先写入临时缓冲区,计算长度后再写入长度复制回来。
BeetleX
开源跨平台通讯框架(支持TLS)
提供HTTP,Websocket,MQTT,Redis,RPC和服务网关开源组件
个人微信:henryfan128 QQ:28304340
有丰富的高吞吐网络服务设计经验
关注公众号
https://github.com/beetlex-io/