最近在优化项目,发现字符串拼接的堆内存非常大,而且非常频繁。
大概原因如下
1.字符串就是char[] 长短发生变化必然要重新分配长度,以及拷贝之前的部分,产生GC
2.字符串+=进行拼接会产生装箱,生成GC
3.Append传入值类型数据,它会调用ToString方法,需要new string 然后把char[]拷进去,又会产生堆内存。
4.new StringBuidler 产生堆内存
5.StringBuidler.ToString 产生堆内存
5.string.format 它内部使用的就是StringBuilder,但是 1)new StringBuilder 2)Append 3)ToString都会产生堆内存。
所以我们需要优化的是
1.
int a = 100;
string b = a + “”;
禁止上述这种写法,会额外产生一次装箱操作。所以要采用如下写法。
string b = a.ToString();
2.少用或者不用string.format,提前缓存共享StringBuilder对象。避免用时候产生堆内存。
3.网上找到一个算法,挺有意思。提前定义好 0 – 9 之间的字符数组,如果传入值类型数据,从高位依次除以10算出每一位的数,然后再去预先声明的0-9字符数组中找对应的char,这样就就不会产生装箱GC了。
4.如果可以提前确定字符串的长度,例如,界面上显示玩家的等级, 我们可以确定level不可能超过3位数也就是100,那么可以提前声明一个长度为3的StringBuilder,通过反射取出来内部的_str,这样就可以避免最后的ToString产生的堆内存了。由于_str内容可能无法擦掉之前的所以需要调用GarbageFreeClear();方法。
1.装箱版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
using System.Text; using UnityEngine; using UnityEngine.Profiling; public class NewBehaviourScript : MonoBehaviour { StringBuilder m_StringBuilder = new StringBuilder(100); string m_StringBuildertxt = string.Empty; private void Start() { m_StringBuildertxt = m_StringBuilder.GetGarbageFreeString(); } private void Update() { int i = Random.Range(0, 100); float f = Random.Range(0.01f, 200.01f); float d = Random.Range(0.01f, 200.01f); string s = "yusong: " + i; Profiler.BeginSample("string.format"); string s1 = string.Format("{0}{1}{2}{3}", i, f, d, s); Profiler.EndSample(); Profiler.BeginSample("+="); string s2 = i +"" + f +"" + d +"" + s; Profiler.EndSample(); Profiler.BeginSample("StringBuilder"); string s3 = new StringBuilder().Append(i).Append(f).Append(d).Append(s).ToString(); Profiler.EndSample(); Profiler.BeginSample("StrExt.Format"); string s4 = StrExt.Format("{0}{1:0.00}{2:0.00}{3}", i, f, d, s); Profiler.EndSample(); Profiler.BeginSample("EmptyGC"); m_StringBuilder.GarbageFreeClear(); m_StringBuilder.ConcatFormat("{0}{1:0.00}{2:0.00}{3}", i, f, d, s); string s5 = m_StringBuildertxt; Profiler.EndSample(); Debug.LogFormat("s1 : {0}",s1); Debug.LogFormat("s2 : {0}", s2); Debug.LogFormat("s3 : {0}", s3); Debug.LogFormat("s4 : {0}", s4); Debug.LogFormat("s5 : {0}", s5); } } |
2.无装箱版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
using System.Text; using UnityEngine; using UnityEngine.Profiling; public class NewBehaviourScript : MonoBehaviour { StringBuilder m_StringBuilder = new StringBuilder(100); string m_StringBuildertxt = string.Empty; private void Start() { m_StringBuildertxt = m_StringBuilder.GetGarbageFreeString(); } private void Update() { int i = Random.Range(0, 100); float f = Random.Range(0.01f, 200.01f); float d = Random.Range(0.01f, 200.01f); string s = "yusong: " + i.ToString(); Profiler.BeginSample("string.format"); string s1 = string.Format("{0}{1}{2}{3}", i.ToString(), f.ToString(), d.ToString(), s); Profiler.EndSample(); Profiler.BeginSample("+="); string s2 = i.ToString() + f.ToString() + d.ToString() + s; Profiler.EndSample(); Profiler.BeginSample("StringBuilder"); string s3 = new StringBuilder().Append(i).Append(f).Append(d).Append(s).ToString(); Profiler.EndSample(); Profiler.BeginSample("StrExt.Format"); string s4 = StrExt.Format("{0}{1:0.00}{2:0.00}{3}", i, f, d, s); Profiler.EndSample(); Profiler.BeginSample("EmptyGC"); m_StringBuilder.GarbageFreeClear(); m_StringBuilder.ConcatFormat("{0}{1:0.00}{2:0.00}{3}", i, f, d, s); string s5 = m_StringBuildertxt; Profiler.EndSample(); Debug.LogFormat("s1 : {0}", s1); Debug.LogFormat("s2 : {0}", s2); Debug.LogFormat("s3 : {0}", s3); Debug.LogFormat("s4 : {0}", s4); Debug.LogFormat("s5 : {0}", s5); } } |
通通过这两张图大家可以看出来装箱和不装箱对GC有多大的影响了,还有最后的无GC版本。代码参考了下面的这两篇文章。
http://www.gavpugh.com/2010/04/05/xnac-a-garbage-free-stringbuilder-format-method/
http://www.gavpugh.com/2010/03/23/xnac-stringbuilder-to-string-with-no-garbage/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
using System; using System.Text; using UnityEngine; public static class StringBuilderExtensions { // These digits are here in a static array to support hex with simple, easily-understandable code. // Since A-Z don't sit next to 0-9 in the ascii table. private static readonly char[] ms_digits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; private static readonly uint ms_default_decimal_places = 5; //< Matches standard .NET formatting dp's private static readonly char ms_default_pad_char = '0'; //! Convert a given unsigned integer value to a string and concatenate onto the stringbuilder. Any base value allowed. public static StringBuilder Concat(this StringBuilder string_builder, uint uint_val, uint pad_amount, char pad_char, uint base_val) { Debug.Assert(pad_amount >= 0); Debug.Assert(base_val > 0 && base_val <= 16); // Calculate length of integer when written out uint length = 0; uint length_calc = uint_val; do { length_calc /= base_val; length++; } while (length_calc > 0); // Pad out space for writing. string_builder.Append(pad_char, (int)Math.Max(pad_amount, length)); int strpos = string_builder.Length; // We're writing backwards, one character at a time. while (length > 0) { strpos--; // Lookup from static char array, to cover hex values too string_builder[strpos] = ms_digits[uint_val % base_val]; uint_val /= base_val; length--; } return string_builder; } //! Convert a given unsigned integer value to a string and concatenate onto the stringbuilder. Assume no padding and base ten. public static StringBuilder Concat(this StringBuilder string_builder, uint uint_val) { string_builder.Concat(uint_val, 0, ms_default_pad_char, 10); return string_builder; } //! Convert a given unsigned integer value to a string and concatenate onto the stringbuilder. Assume base ten. public static StringBuilder Concat(this StringBuilder string_builder, uint uint_val, uint pad_amount) { string_builder.Concat(uint_val, pad_amount, ms_default_pad_char, 10); return string_builder; } //! Convert a given unsigned integer value to a string and concatenate onto the stringbuilder. Assume base ten. public static StringBuilder Concat(this StringBuilder string_builder, uint uint_val, uint pad_amount, char pad_char) { string_builder.Concat(uint_val, pad_amount, pad_char, 10); return string_builder; } //! Convert a given signed integer value to a string and concatenate onto the stringbuilder. Any base value allowed. public static StringBuilder Concat(this StringBuilder string_builder, int int_val, uint pad_amount, char pad_char, uint base_val) { Debug.Assert(pad_amount >= 0); Debug.Assert(base_val > 0 && base_val <= 16); // Deal with negative numbers if (int_val < 0) { string_builder.Append('-'); uint uint_val = uint.MaxValue - ((uint)int_val) + 1; //< This is to deal with Int32.MinValue string_builder.Concat(uint_val, pad_amount, pad_char, base_val); } else { string_builder.Concat((uint)int_val, pad_amount, pad_char, base_val); } return string_builder; } //! Convert a given signed integer value to a string and concatenate onto the stringbuilder. Assume no padding and base ten. public static StringBuilder Concat(this StringBuilder string_builder, int int_val) { string_builder.Concat(int_val, 0, ms_default_pad_char, 10); return string_builder; } //! Convert a given signed integer value to a string and concatenate onto the stringbuilder. Assume base ten. public static StringBuilder Concat(this StringBuilder string_builder, int int_val, uint pad_amount) { string_builder.Concat(int_val, pad_amount, ms_default_pad_char, 10); return string_builder; } //! Convert a given signed integer value to a string and concatenate onto the stringbuilder. Assume base ten. public static StringBuilder Concat(this StringBuilder string_builder, int int_val, uint pad_amount, char pad_char) { string_builder.Concat(int_val, pad_amount, pad_char, 10); return string_builder; } //! Convert a given float value to a string and concatenate onto the stringbuilder public static StringBuilder Concat(this StringBuilder string_builder, float float_val, uint decimal_places, uint pad_amount, char pad_char) { Debug.Assert(pad_amount >= 0); if (decimal_places == 0) { // No decimal places, just round up and print it as an int // Agh, Math.Floor() just works on doubles/decimals. Don't want to cast! Let's do this the old-fashioned way. int int_val; if (float_val >= 0.0f) { // Round up int_val = (int)(float_val + 0.5f); } else { // Round down for negative numbers int_val = (int)(float_val - 0.5f); } string_builder.Concat(int_val, pad_amount, pad_char, 10); } else { int int_part = (int)float_val; // First part is easy, just cast to an integer string_builder.Concat(int_part, pad_amount, pad_char, 10); // Decimal point string_builder.Append('.'); // Work out remainder we need to print after the d.p. float remainder = Math.Abs(float_val - int_part); // Multiply up to become an int that we can print do { remainder *= 10; decimal_places--; } while (decimal_places > 0); // Round up. It's guaranteed to be a positive number, so no extra work required here. remainder += 0.5f; // All done, print that as an int! string_builder.Concat((uint)remainder, 0, '0', 10); } return string_builder; } //! Convert a given float value to a string and concatenate onto the stringbuilder. Assumes five decimal places, and no padding. public static StringBuilder Concat(this StringBuilder string_builder, float float_val) { string_builder.Concat(float_val, ms_default_decimal_places, 0, ms_default_pad_char); return string_builder; } //! Convert a given float value to a string and concatenate onto the stringbuilder. Assumes no padding. public static StringBuilder Concat(this StringBuilder string_builder, float float_val, uint decimal_places) { string_builder.Concat(float_val, decimal_places, 0, ms_default_pad_char); return string_builder; } //! Convert a given float value to a string and concatenate onto the stringbuilder. public static StringBuilder Concat(this StringBuilder string_builder, float float_val, uint decimal_places, uint pad_amount) { string_builder.Concat(float_val, decimal_places, pad_amount, ms_default_pad_char); return string_builder; } //! Concatenate a formatted string with arguments public static StringBuilder ConcatFormat<A>(this StringBuilder string_builder, String format_string, A arg1) where A : IConvertible { return string_builder.ConcatFormat<A, int, int, int>(format_string, arg1, 0, 0, 0); } //! Concatenate a formatted string with arguments public static StringBuilder ConcatFormat<A, B>(this StringBuilder string_builder, String format_string, A arg1, B arg2) where A : IConvertible where B : IConvertible { return string_builder.ConcatFormat<A, B, int, int>(format_string, arg1, arg2, 0, 0); } //! Concatenate a formatted string with arguments public static StringBuilder ConcatFormat<A, B, C>(this StringBuilder string_builder, String format_string, A arg1, B arg2, C arg3) where A : IConvertible where B : IConvertible where C : IConvertible { return string_builder.ConcatFormat<A, B, C, int>(format_string, arg1, arg2, arg3, 0); } //! Concatenate a formatted string with arguments public static StringBuilder ConcatFormat<A, B, C, D>(this StringBuilder string_builder, String format_string, A arg1, B arg2, C arg3, D arg4) where A : IConvertible where B : IConvertible where C : IConvertible where D : IConvertible { int verbatim_range_start = 0; for (int index = 0; index < format_string.Length; index++) { if (format_string[index] == '{') { // Formatting bit now, so make sure the last block of the string is written out verbatim. if (verbatim_range_start < index) { // Write out unformatted string portion string_builder.Append(format_string, verbatim_range_start, index - verbatim_range_start); } uint base_value = 10; uint padding = 0; uint decimal_places = 5; // Default decimal places in .NET libs index++; char format_char = format_string[index]; if (format_char == '{') { string_builder.Append('{'); index++; } else { index++; if (format_string[index] == ':') { // Extra formatting. This is a crude first pass proof-of-concept. It's not meant to cover // comprehensively what the .NET standard library Format() can do. index++; // Deal with padding while (format_string[index] == '0') { index++; padding++; } if (format_string[index] == 'X') { index++; // Print in hex base_value = 16; // Specify amount of padding ( "{0:X8}" for example pads hex to eight characters if ((format_string[index] >= '0') && (format_string[index] <= '9')) { padding = (uint)(format_string[index] - '0'); index++; } } else if (format_string[index] == '.') { index++; // Specify number of decimal places decimal_places = 0; while (format_string[index] == '0') { index++; decimal_places++; } } } // Scan through to end bracket while (format_string[index] != '}') { index++; } // Have any extended settings now, so just print out the particular argument they wanted switch (format_char) { case '0': string_builder.ConcatFormatValue<A>(arg1, padding, base_value, decimal_places); break; case '1': string_builder.ConcatFormatValue<B>(arg2, padding, base_value, decimal_places); break; case '2': string_builder.ConcatFormatValue<C>(arg3, padding, base_value, decimal_places); break; case '3': string_builder.ConcatFormatValue<D>(arg4, padding, base_value, decimal_places); break; default: Debug.Assert(false, "Invalid parameter index"); break; } } // Update the verbatim range, start of a new section now verbatim_range_start = (index + 1); } } // Anything verbatim to write out? if (verbatim_range_start < format_string.Length) { // Write out unformatted string portion string_builder.Append(format_string, verbatim_range_start, format_string.Length - verbatim_range_start); } return string_builder; } //! The worker method. This does a garbage-free conversion of a generic type, and uses the garbage-free Concat() to add to the stringbuilder private static void ConcatFormatValue<T>(this StringBuilder string_builder, T arg, uint padding, uint base_value, uint decimal_places) where T : IConvertible { switch (arg.GetTypeCode()) { case System.TypeCode.UInt32: { string_builder.Concat(arg.ToUInt32(System.Globalization.NumberFormatInfo.CurrentInfo), padding, '0', base_value); break; } case System.TypeCode.Int32: { string_builder.Concat(arg.ToInt32(System.Globalization.NumberFormatInfo.CurrentInfo), padding, '0', base_value); break; } case System.TypeCode.Single: { string_builder.Concat(arg.ToSingle(System.Globalization.NumberFormatInfo.CurrentInfo), decimal_places, padding, '0'); break; } case System.TypeCode.String: { string_builder.Append(Convert.ToString(arg)); break; } default: { Debug.Assert(false, "Unknown parameter type"); break; } } } public static void Empty(this StringBuilder string_builder) { string_builder.Remove(0, string_builder.Length); } public static string GetGarbageFreeString(this StringBuilder string_builder) { return (string)string_builder.GetType().GetField("_str", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(string_builder); } public static void GarbageFreeClear(this StringBuilder string_builder) { string_builder.Length = 0; string_builder.Append(' ', string_builder.Capacity); string_builder.Length = 0; } } public static class StrExt { //只允许拼接50个字符串 private static StringBuilder s_StringBuilder = new StringBuilder(50, 50); public static string Format<A>(String format_string, A arg1) where A : IConvertible { s_StringBuilder.Empty(); return s_StringBuilder.ConcatFormat<A, int, int, int>(format_string, arg1, 0, 0, 0).ToString(); } public static string Format<A, B>(String format_string, A arg1, B arg2) where A : IConvertible where B : IConvertible { s_StringBuilder.Empty(); return s_StringBuilder.ConcatFormat<A, B, int, int>(format_string, arg1, arg2, 0, 0).ToString(); } public static string Format<A, B, C>(String format_string, A arg1, B arg2, C arg3) where A : IConvertible where B : IConvertible where C : IConvertible { s_StringBuilder.Empty(); return s_StringBuilder.ConcatFormat<A, B, C, int>(format_string, arg1, arg2, arg3, 0).ToString(); } public static string Format<A, B, C, D>(String format_string, A arg1, B arg2, C arg3, D arg4) where A : IConvertible where B : IConvertible where C : IConvertible where D : IConvertible { s_StringBuilder.Empty(); return s_StringBuilder.ConcatFormat<A, B, C, D>(format_string, arg1, arg2, arg3, arg4).ToString(); } } |
- 本文固定链接: https://www.xuanyusong.com/archives/4601
- 转载请注明: 雨松MOMO 于 雨松MOMO程序研究院 发表
StrExt.Format 貌似没有处理带”#”号的format
发现如下问题:
1、把上面的脚本在空unity下跑会报错空引用,
private void Start()
{
m_StringBuildertxt = m_StringBuilder.GetGarbageFreeString();
}
public static string GetGarbageFreeString(this StringBuilder string_builder)
{
return (string)string_builder.GetType().GetField(“_str”, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(string_builder);
}
方法GetField()肯能空
2、Profiler.BeginSample(“EmptyGC”);
m_StringBuilder.GarbageFreeClear();
m_StringBuilder.ConcatFormat(“{0}{1:0.00}{2:0.00}{3}”, i, f, d, s);
string s5 = m_StringBuildertxt;
Profiler.EndSample();
这里s5一直为空字符串,如果m_StringBuilder.ToString()还是有GC
博主你好,unity版本是2018.4,运行无装箱例子的时候报错,反射的时候获取stringbuilder的_str为null。
NullReferenceException: Object reference not set to an instance of an object
StringBuilderExtensions.GetGarbageFreeString (System.Text.StringBuilder string_builder) (at Assets/LuaFramework/Scripts/Utility/StringBuilderExtensions.cs:368)
NewBehaviourScript.Start () (at Assets/LuaFramework/Scripts/NewBehaviourScript.cs:12)
.net4.0里StringBuilder的实现改了,没有了成员变量_str,改成了char[]类型。。所以要想用这个优化.net只能用3.5以下。。
感谢你的回答,