首页 >Unity3D频道 >【Unity3D研究院之游戏开发】 > Unity3D研究院之字符串拼接0GC(一百零四)
2019
06-10

Unity3D研究院之字符串拼接0GC(一百零四)

最近在优化项目,发现字符串拼接的堆内存非常大,而且非常频繁。
大概原因如下
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.装箱版本

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);
    }
}

Snip20190610_1

2.无装箱版本

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);
    }
}

WechatIMG13

通通过这两张图大家可以看出来装箱和不装箱对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/

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();
    }
}
最后编辑:
作者:雨松MOMO
专注移动互联网,Unity3D游戏开发
捐 赠写博客不易,如果您想请我喝一杯星巴克的话?就进来看吧!

Unity3D研究院之字符串拼接0GC(一百零四)》有 5 条评论

  1. StrExt.Format 貌似没有处理带”#”号的format

  2. MiKiNuo说:

    发现如下问题:
    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

  3. foxxof说:

    博主你好,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)

留下一个回复

你的email不会被公开。 必填项已用 * 标注。