内容简介:For .NET Core and Unity, I have released a library calledI am the author of high-performance serializers such as
ZString — Zero Allocation StringBuilder for .NET Core and Unity.
Feb 25 ·7min read
For .NET Core and Unity, I have released a library called ZString that enables the memory allocation to zero for string generation. Besides introducing ZString, this article also deeply disassembles and explains the C# string and explains the String’s complexities and pitfalls and the necessity of ZString.
https://github.com/Cysharp/ZString
I am the author of high-performance serializers such as MessagePack for C# and Utf8Json . This is one of them, for performance enthusiasts.
The table below shows the performance measurement of a simple string concatenation called “x:” + x + “ y:” + y + “ z:” + z’.
- “x:” + x + ” y:” + y + ” z:” + z
- ZString.Concat(“x:”, x, ” y:”, y, ” z:”, z)
- string.Format(“x:{0} y:{1} z:{2}”, x, y, z)
- ZString.Format(“x:{0} y:{1} z:{2}”, x, y, z)
- new StringBuilder(), Append(), .ToString()
- ZString.CreateStringBuilder(), Append(), .ToString()
The graph shows the respective memory allocation amount and speed for the above. In all cases, ZString does not have any allocation other than 56B strings after concatenation. Also, for easy API use, ‘StringBuilder’ or ‘String.Format’, ‘String.Concat’, and ‘String.Join’ can be replaced as is.
String type configuration and generation
The C# String type is internally a ‘UTF-16’ byte string.
As with a normal object, it has an object header, and allocated in heap memory. In the same way, string is basically only be generated by ‘new string’. ‘StringBuilder.ToString’, ‘Encoding.GetString’, etc., also finally call ‘new string’ to allocate a new string.
To be precise, some .NET internal methods directly write to the string memory allocated by an internal method called ‘String.FastAllocateString(int length)’. This method has not been public. However, .NET Standard 2.1 adds the ‘String.Create<TState>(int length, TState state, SpanAction<char, TState> action)’ method. By calling this, direct writing to a new string memory is possible.
The string generated by ‘new string’ is allocated in a different memory space if even the same string value. However, only the constant string acquires the fixed reference from the application-sharing space called the Intern pool.
If you want to acquire from the intern pool, ‘String.Intern’ method can be used. The Intern method is acquired from the Intern pool. If it does not exist, it is registered and the registered reference is returned.
Since the memory registered in the Intern pool cannot be deleted, it will probably be difficult to use it well. However, with the [ MasterMemory ] in-memory database being developed by our company, a string data that expanded in memory as the master-data it will be persisted in application lifetime. By using this feature, all the strings are internalized.
Also, with the .NET Core’s runtime, there is a proposal to have a function called [ String Deduplication ] to delete the string duplicated during GC (convert to a single reference). However, it will still take a while longer to implement it.
+ concatenation and String.Concat
The C# compiler does specialized processing and String’s ‘+’ concatenation is converted to String.Concat.
Since ‘“x:” + x + “ y:” + y + “ z:” + z’ is a 6-parameter concatenation, it is converted to ‘string.Concat(string[] values)’. (In the case of Visual Studio 2019 Version 16.4.2’s C# compiler. Details later.) In other words, the result will be as follows.
Optimizing the ‘+’ concatenation with the C# compiler may obtain a different result between the current one and past one. For example, Visual Studio 2019’s C# compiler’s result of (int x) + (string y) + (int z) will be ‘String.Concat(x.ToString(), y, z.ToString())’. However, Visual Studio 2017’s C# compiler will be ‘String.Concat((object)x, y, (object)z)’, if concatenated non-string parameter, object overload will be used. Therefore, struct boxing occurs. If Unity is used, you must note that the result will differ depending on the version of the C# compiler bundled with Unity.
As Concat’s overload, three or four parameter optimized by avoid ‘params array` allocation.
StringBuilder and SpanFormatter
‘StringBuilder’ is a class that has ‘char[]’ as a temporary buffer. Append is used to write to the buffer, and ToString generates the final string.
When if you want to concatenate multiple Strings, should avoid ‘+=’ to use because a new string is generated for each ‘+=’.
StringBuilder avoids generating this temporary, new string and instead copies it to ‘char[]’.
Here, you need to watch out for ‘sb.Append(“ Current HP:” + enemy.Hp);’ etc., being written because it will create a temporary string that was concatenated. By all means, avoid using ‘+’.
When Appending numeric type, etc., the behavior will be different between .NET Standard 2.0 (Unity, etc.) and .NET Standard 2.1 (.NET Core 3.0, etc.).
With .NET Standard 2.0, it simply adds the ToString result. In other words, although an allocation is created by the string creation, with .NET Standard 2.1, ‘ISpanFormattable.TryFormat’ writes it directly to the buffer without going through the string. ISpanFormattable itself is internal. However, by checking [ ISpanFormattable.references ], you can see which type is implementing this direct writing.
Even in the Unity environment, the string allocation when the numeric type is added can be avoided by ZString. In .NET Standard 2.1, ZString uses their implemented TryFormat. In .NET Standard 2.0, ZString uses ported TryFormat method.
The API itself is almost the same as StringBuilder. However, it must be enclosed by “using.”
Since CreateStringBuilder’s return value ‘Utf16ValueStringBuilder’ is a struct, allocation to the StringBuilder’s heap is avoided. Also, since the ‘char[]’ buffer used for internal writing is obtained from ArrayPool, the buffer allocation is avoided. (However, this is why it is necessary to return the buffer by “using”.)
Also, ‘ZString.Concat’ uses ‘Utf16ValueStringBuilder’ internally. And since generic overload up to 15 parameters is provided, the numeric type’s string conversion allocation can be completely avoided even in the Unity environment.
Format and ReadOnlySpan<char>
Since String interpolation is converted into String.Format, it has no overhead. However, since String.Format’s parameter can only accept objects, boxing occurs.
Also, as with StringBuilder.Append, with .NET Standard 2.0, string conversion allocation also occurs.
Like ‘ZString.Concat’, ‘ZString.Format’ has generic overload up to 15 parameters. And even in the .NET Standard 2.0 environment, by implementing direct conversion with TryFormat, Zero Allocation is achieved.
‘String.Format’ supports [ Composite Formatting ] . The composite-formatted string expressed by ‘{ index[,alignment][:formatString]}’ can be used to format the date or set the number of digits for numeric values.
ZString.Format supports formatString, but not alignment.
In the end, this composite-formatted string is sampled as ‘.##’ and given to TryFormat. Let’s again look at the definition of ISpanFormattable.
Note that instead of being ‘string format’, it is ‘ReadOnlySpan<char> format’. It is because the format string obtained when the string is analyzed is a partial slice. If you take the string in the above example, it is divided into [x:], [.##], [, y:], and [0000.#] slices. The [x:] and [, y:] are copied as is, and [.##] and [0000.#] are given as a format strings to TryFormat. In this way, by expressing the format string as ‘ReadOnlySpan<char>’, the string allocation is avoided.
As explained in the beginning above, since the String is actually a Utf-16 byte string, it can be expressed by ‘ReadOnlySpan<char>/Span<char>’. Unlike ‘string’, ‘ReadOnlySpan<char>’ enables partial acquisition and can use ‘char[]’. Therefore, it is easy to use from the pool.
And so, providing an API that accepts the string as ‘ReadOnlySpan<char>’ would make it easy to improve performance. However, due to ‘ref struct’, it cannot be retained in the field and ‘.NET Standard 2.0’ does not provide implicit conversion for ‘string -> ReadOnlySpan<char>’. Therefore, the usability is adversely affected. It is necessary to keep this in mind for the design.
Direct writing without allocate String
ZString’s internal implementation is zero allocation. When the string is generated in the end, it is allocated. This is because most APIs request a string. However, if the applicable library has an API that accepts something other than a string, this final string generation is also avoided and a completely zero allocation can be attained. For example, since Unity’s TextMeshPro has an API called ‘SetCharArray(char[] sourceText, int start, int length)’, it can be given directly and the string generation can be avoided.
Even with .NET Core, APIs that can accept ‘ReadOnlySpan<char>’ are increasing. In this way, by having more approaches that avoid the string, the application’s overall performance should improve.
Utf8String and ‘ReadOnlySpan<byte>’
In the network or file I/O, in many cases, the data requested in the end is not String (UTF16), but byte[](UTF8). In such a case, if it is ‘Encoding.UTF8.GetBytes(stringBuilder.ToString())’, char[] writing → string generation → UTF8 encoding will be quite useless. If it is written directly in UTF8, the overhead will be zero. Therefore, ZString provides the ‘CreateUtf8StringBuilder’ method.
We can expect it to be effectively used for generating matrix data to be sent to a network that needs to be frequently written or be effectively used as the template engine’s backend.
To write directly to the numeric type’s Utf8, ‘System.Buffers.Text.Utf8Formatter’ is used. They have the TryFormat method to write to ‘Span<byte>’.
In other words, like string(Utf16) expressing ‘ReadOnlySpan<char>’, Utf8 expresses ‘ReadOnlySpan<byte>’.
Summary
String is a basic type. Since it is a basic type, it looks simple while having many special operations. And just by using it normally, it is impossible to avoid all the pitfalls. ZString offers an API similar to String/StringBuilder. Therefore, instead of thinking of the complicated details given here, it is designed to just simply replace it to give the best performance. Iwould be happy if you all tried it.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C++设计新思维
(美)Andrei Alexandrescu / 侯捷、於春景 / 华中科技大学出版社 / 2003-03 / 59.8
本书从根本上展示了generic patterns(泛型模式)或pattern templates(模式模板),并将它们视之为“在C++中创造可扩充设计”的一种功能强大的新方法。这种方法结合了template和patterns,你可能未曾想过,但的确存在。为C++打开了全新视野,而且不仅仅在编程方面,还在于软件设计本身;对软件分析和软件体系结构来说,它也具有丰富的内涵。一起来看看 《C++设计新思维》 这本书的介绍吧!