官术网_书友最值得收藏!

2.8.4 字符串導致的性能問題

本質上,字符串性能問題在大部分語言中都是比較難解決的,C#中尤其如此。在C#中,string是引用類型,每次動態創建一個string,C#都會在堆內存中分配一個內存用于存放字符串。我們來看看它到底有多么“恐怖”,其源碼如下:


string strA = "test";
for(int i = 0 ; i<100 ; i++)
{
    string strB = strA + i.ToString();

    string[] strC = strB.Split('e');

    strB = strB + strC[0];

    string strD = string.Format("Hello {0}, this is {1} and {2}.",strB, strC[0], 
        strC[1]);
}

這是一段“恐怖”的程序,循環中每次都會將strA字符串和i整數字符串連接,strB所得到的值是從內存中新分配的字符串,然后將strB切割成兩半,使其成為strC,這兩半又重新分配兩段新的內存,再將strB與strC[0]連接起來,這又申請了一段內存,這段內存裝上strB和strC[0]連接的內容,并賦值給strB,strB原來的內容因為沒有變量指向就找不到了,最后用string.Format的形式將4個字符串串聯起來,新分配的內存中裝有4者的連接內容。

這里要注意一點,字符串常量是不會被丟棄的,比如這段程序中的"test"和"Hello {0}, this is {1} and {2}."這兩個常量,它們常駐于內存,即使下次沒有變量指向它們,它們也不會被回收,下次使用時也不需要重新分配內存。關于原因,我們放到計算機執行原理中介紹。

每次循環都向內存申請了5次內存,并且拋棄了一次strA+i.ToString()的字符串內容,這是因為沒有變量指向這個字符串。這還不是最“恐怖”的,最“恐怖”的是,每次循環結束都會將前面所有分配的內存內容拋棄,再重新分配一次,就這樣不斷地拋棄和申請,總共向內存申請了500次內存段,并全部拋棄,內存被浪費得很厲害。

為什么會這樣呢?究其原因是,C#語言對字符串并沒有任何緩存機制,每次使用都需要重新分配string內存,據我所知,很多語言都沒有字符串的緩存機制,因此字符串連接、切割、組合等操作都會向內存申請新的內存,并且拋棄沒有變量指向的字符串,等待GC單元回收。我們知道,GC單元執行一次會消耗很多CPU空間,如果不注意字符串的問題,不斷浪費內存,則將導致程序不定時卡頓,并且,隨著程序運行時間的加長,各程序模塊不良代碼的運行積累,程序卡頓次數會逐步增加,運行效率也將越來越低。

解決字符串問題有兩種方法。

第一種方法是自建緩存機制,可以用一些標志性的Key值來一一對應字符串,比如游戲項目中常用ID來構造某個字符串,偽代碼如下:


int ID = 101;

ResData resData = GetDataById(ID);

string strName = "This is " + resData.Name;

return strName;

一個ID變量對應一個字符串,這種形式下可以建立一個字典容器將它緩存起來,下次用的時候就不需要重新申請內存了,偽代碼如下:


Dictionary<int,string>strCache;

string strName = null;
if(!strCache.TryGetValue(id, out strName))
{
    ResData resData = GetDataById(ID);
    string strName = "This is " + resData.Name;
    strCache.Add(id, strName);
}

return strName;

我們用Dictionary字典容器將字符串緩存起來,每次先查詢字典中的內容是否存在,若有,則直接使用,若沒有,則創建一個并將其植入字典容器中,以便下次使用。

第二種方法需要用到C#中一些“不安全”的native方法,也就是類似C++的指針方式來處理string類。

由于string類本身一定會申請新的內存,因此需要突破這個瓶頸,直接使用指針來改變string中字符串的值,這樣就能重復利用string,而不需要重新分配內存。

C#雖然委托了大部分內存內容,但它也允許我們使用非委托的方式來訪問和改變內存內容,這對C#來說是不安全的(C#中有unsafe關鍵字)。下面通過非委托的方式來改變string中的內容,使它能夠被我們再利用,代碼如下:


string strA = "aaa";

string strB = "bbb" + "b";

fixed(char* strA_ptr = strA)
{
    fixed(char* strB_ptr = strB)
    {
        memcopy((byte*)strB_ptr, (byte*)strA_ptr, 3*sizeof(char));
    }
}

print(strB); // 此時strB的內容為“aaab”

注意,這里用“bbb”+“b”的方式生成新字符串,是因為我們不打算改變常量字符串內存塊,所以新分配了內存來做實驗。

我們把strB的前3個字符的內容變成了strA中的內容,但并沒有增加其他內存,因為我們使用了不安全的非托管方法來控制內存。通過這樣的方式再利用已經申請的字符串內存,可將已有的字符串緩存起來再利用。我們看看再利用的例子,其源碼如下:


Dictionary<int,string>cacheStr;

public unsafe string Concat(string strA, string strB)
{
    int a_length = a.Length;

    int b_length = b.Length;

    int sum_length = a_Length + b_Length;

    string strResult = null;

    if(!cacheStr.TryGetValue(sum_length, out strResult))
    {
        // 如果不存在sum_length長度的緩存字符串,那么直接連接后存入緩存
        strResult = strA + strB;

        cacheStr.Add(sum_length, strResult);

        return strResult;
    }

    // 將緩存字符串再利用,用指針方式直接改變它的內容
    fixed(char* strA_ptr = strA)
    {
        fixed(char* strB_ptr = strB)
        {
            fixed(char* strResult_ptr = strResult)
            {
                // 將strA中的內容復制到strResult中
                memcopy((byte*)strResult_ptr, (byte*)strA_ptr,
                    a_length*sizeof(char));

                // 將strB中的內容復制到strResult的a_Length長度后的內存中
                memcopy((byte*)strResult_ptr+a_Length,
                    (byte*)strB_ptr, b_length*sizeof(char));
            }
        }
    }

    return strResult;
}

當需要將多個字符串連接起來時,先看看緩存中是否有可用長度的字符串,如果沒有,就直接連接并緩存,如果有,則取出來,使用指針的方式改變緩存字符串的值。其中memcopy并不是系統函數,因此需要自己編寫,寫法很簡單,拿到兩個指針根據長度遍歷并賦值即可。源碼如下:


public unsafe void memcopy(byte* dest, byte* src, int len)
{
    while((--len)>=0)
    {
        dest[len] = src[len];
    }
}

主站蜘蛛池模板: 资源县| 邳州市| 阳泉市| 阿图什市| 灯塔市| 兴隆县| 始兴县| 修武县| 颍上县| 泾源县| 泾阳县| 泸定县| 清徐县| 龙门县| 错那县| 玉山县| 临高县| 芷江| 樟树市| 昌黎县| 正蓝旗| 开阳县| 延边| 图木舒克市| 图木舒克市| 万安县| 故城县| 宣化县| 泾阳县| 饶平县| 玛曲县| 甘肃省| 大洼县| 阿拉尔市| 和龙市| 潮州市| 治县。| 河北区| 商丘市| 江西省| 榆中县|