临时字符串拼接的优化

字符串拼接在高级语言中事件很让人头疼的事情,无论是内存申请的额外耗时还是产生临时内存造成的大量gc都是很难优化的,最近在c#中采用了一种优化方案,在此记录一下。

造成性能灾难的原因

首先我们要明白c#对字符串的处理是怎么样的:

c#对一些通常的字符串操作,是做了缓存的优化的,string作为一个引用类型,不同的变量只要内容相同,实质上都是指向同一个引用,我们可以用下述方法获取变量内存地址来一探究竟:

public string getMemory(object o) // 获取引用类型的内存地址方法
{
        GCHandle h = GCHandle.Alloc(o, GCHandleType.Pinned);

        IntPtr addr = h.AddrOfPinnedObject();

        return "0x" + addr.ToString("X");
}
string s1 = "111";
string s2 = "11" + "1";
string s3 = new string('1', 3);
Debug.Log("s1:" + s1);
Debug.Log("s2:" + s2);
Debug.Log("s3:" + s3);
Debug.Log("s1 memory:" + getMemory(s1));
Debug.Log("s2 memory:" + getMemory(s2));
Debug.Log("s3 memory:" + getMemory(s3));

观察输出可以发现,字符串s1,s2虽然是通过不同方式拼接的,但是因为其内容一致,引用指向的内存地址都是相同的。

c#在常用的字符串操作中都采用了这样的优化方式,但是可以通过new string的方式来构建一个新的string引用,可以看到上面的字符串s3虽然内容与s1,s2都相同,但是地址却是不一样的。

在字符串拼接时,实质上并没有对原有字符串做任何改变,而是检查是否全局已经缓存了相同字符串,如果缓存了就返回引用,没有的话便重新创建一个新的string,直到下一次gc被回收,这样也就解释了为什么我们拼接不同结果字符串的时候会产生大量的内存垃圾。

string的本质

想要优化字符串拼接的话,我们需要进一步探究string的本质:

c#中的string是一个引用类型的class,其本质是一个含有一些描述信息的char数组,但是我们对本质的探究到这里并没有停止。

对业务层而言,我们需要的字符串到底是什么?按最终用途可以分成两类:

最终变为二进制流的数据

业务中一大部分的字符串,最终实际上都会转变为数据流,比如网络消息和写文件。实际上我们并不需要产生string类,使用自己维护的char数组来记录字符数据,然后直接输出为二进制流,之后再对char数组进行回收重用,可以彻底解决string类产生的gc和内存压力。

需要注意的是,如果需要对字符流编码,可以使用encoder的原生方法:

Encoder mEncoder = Encoding.UTF8.GetEncoder();
    private unsafe int GetBytes(char[] chars,byte[] bytes,int offset,int length,int outOffset)
    {
        fixed (char* c = chars)
        {
            //最后一个参数代表是否刷新encode缓存内容
            int count = mEncoder.GetByteCount(c + offset, length, false);
            //注意byte数组要开辟足够的空间
            fixed (byte* b = bytes)
            {
                mEncoder.GetBytes(c + offset, length, b + outOffset, count, false);
                return count;
            }
        }
    }

最终需要传递给系统API的string

对于业务中需要传递给不可控接口的string,我们需要对接口需要的string进行判断,然后通过修改string内容的方式,对string这一类进行重用。要知道虽然c#没有提供string内容修改的接口,但是string本质上也还是char数组,对其内容修改相当容易:

private unsafe void SetChar(string s, char c, int index)
        {
            if (index >= s.Length)
                throw new ArgumentOutOfRangeException("index");

            fixed (char *p = s) {
                // Set the character.
                p[index] = c;
            }
        }

缓存的策略类似普通的缓存池,需要新字符串时,从string池中取出一个长度足够的(不够就新建),从头填充正确的数据,最后对多余的尾部按需修改填充,用完放回池子。

渲染类

对于渲染类的string一般都好处理,重用string时尾部填充‘\0'就可以,例如unity中的Text组件就可以采用这样的策略。(unity在读到\0后就不读了,所以实际上只要把不需要的第一个位置填上\0就好)

json类

对于json这样的字符串标记语言来说就要麻烦一些了,如果项目中的json解析类是可以修改的,还可以用\0来表示终止,但是遇到外部库这样的,不知道是否对\0做处理的话,只能在数据传输时约定一个特殊key,内容填空字符串,通过这样的方式来补全string多余的无用位置了。

你可能感兴趣的:(临时字符串拼接的优化)