代碼中的魔鬼細節

軟件開發最關心的三個指標:性能、內存、程序穩定性三方面。本文總結一下最近項目掃尾工作中的一些遭遇:


使用正確的哈希函數

道路的路況繪製,道路的顏色由三個ID唯一確定,他們存儲在一個哈希表中。


上圖是兩種哈希函數的性能對比。badHashFunction的結果爲藍色,goodHashFunction的結果爲紅色曲線。

使用壞的哈希函數,執行DJB_hash的結果衝突可能性十分大,因此哈希的平均查找次數非常大,在性能很好的機器上拖動時也有明顯的卡頓現象。

優化哈希函數,將三個ID的所有位數拼接成一個數字串,然後傳入DJB_hash結果十分好,性能得到質的提升。

static unsigned int DJB_hash(int* buffer, int len) 
{
	unsigned int hash = 5381;
	int i = 0;

	while (i < len) {
		hash += (hash << 5) + buffer[i++];
	}

	return (hash & 0x7FFFFFFF);
}

static unsigned int badHashFunction(const void* key)
{
	rtic_t* ptr = (rtic_t*)key;

	int buffer[3];
	buffer[0] = ptr->mapId - RTIC_MIN_MAPID;
	buffer[1] = ptr->kind;
	buffer[2] = ptr->middle;
	
	return DJB_hash(buffer, 3);
}
static unsigned int goodHashFunction(const void* key)
{
	rtic_t* ptr = (rtic_t*)key;

    const int SIZE = 20;
	int buffer[SIZE] = {0};

    int len = 0;

    int v = ptr->mapId;
    while (v && len < SIZE)
    {
        buffer[len++] = v % 10;
        v /= 10;
    }
    
    v = ptr->kind;
    while (v && len < SIZE)
    {
        buffer[len++] = v%10;
        v /= 10;
    }

    v = ptr->middle;
    while (v && len < SIZE)
    {
        buffer[len++] = v%10;
        v /= 10;
    }
	
	return DJB_hash(buffer, len);
}


謹慎使用vector

Vector的好處就是動態增長,使用起來非常方便,只管不斷地push_back,不用關心內存增加的細節。Vector稍微有經驗的都知道,push_back之前應該先reserve。這麼做效率更高!

最近查我們項目的樣式配置模塊,突然發現內存佔用十分厲害,前後情況如下:

樣式加載前內存情況:


樣式加載後內存情況:


單純樣式模塊佔用600K

一路追查下去,結果哭笑不得。我們自己實現了一套C風格的Vector,裏面存儲void*指針從而實現泛型。Vector內部reserve函數實現非常之坑爹,代碼片段如下,每次reserve結果vector中至少有256個指針。

void TXVector::reserve(TXUINT32 capacity)
{
	if (capacity <= _capacity)
	{
		return;
	}
    
	_capacity = capacity * 2;
	if (_capacity < 256)
	{
		_capacity = 256;
	}


上圖爲樣式配置模塊的數據結構,是個二維數組。當時因爲趕項目進度,regionStyleList的每個成員內部有一個vector,然而vector只保存1-3個指針成員。RegionStyleList的數量很大,所以導致內存浪費十分嚴重。

優化時直接KISS(keep it simple and stupid)化,採用最簡單的二維數組優化後,結果非常明顯,內存降到82K


注意空指針訪問問題

背景:地圖切換數據後,城市的路況映射表達到500k左右,於是我們對數組進行了延遲創建處理。就是因爲這個優化引入了一個十分隱晦的BUG,灰度上線後IOS、Android兩個平臺都收到不同數量的crash日誌。下面是android的日誌:

********** Crash dump: **********

Build fingerprint: 'samsung/t03gzs/t03g:4.1.2/JZO54K/N7100ZSDMA6:user/release-keys'

pid: 10190, tid: 10190  >>> com.XX.map <<<

signal 11 (SIGSEGV), fault addr 00000000

Stack frame #00  pc 0000e270  /system/lib/libc.so (memcpy)

日誌中黃色部分標識空指針訪問越界,Crash代碼行指向memcpy這一行:


這個空指針crash正常情況下不會發生,如下圖我們升級因爲要兼容舊數據,所以程序中有兩種路徑:


每條豎直路徑表示我們預期的正常途徑:全量更新時創建數組,增量更新時刷新數組。紅色箭頭路徑表示非法路徑:當A格式全量更新創建A格式數組,然後下次增量更新時跳至了B的路徑去刷新B的數組,此時B的數組爲空,從而空指針CRASH。

界面層的某種操作會觸發紅色路徑!所以空指針crash帶有隨機性色彩。通過不斷討論我們最終歸納出了BUG必現的觸發途徑。當然了空指針BUG解決起來非常容易。


注意對程序邊界條件處理

引擎重構以後,兩個平臺多了一種crash日誌,android內容:

Build fingerprint: 'samsung/m0zs/m0:4.1.2/JZO54K/I9300ZSEMC1:user/release-keys'

pid: 26598, tid: 26598  >>> com.XXX.map <<<

signal 11 (SIGSEGV), fault addr 04000000

Stack frame #00  pc 0000e264  /system/lib/libc.so (memcpy)

Stack frame #01  pc 0001a780  /data/data/com.XXX.map/lib/libengine.so: Routine getScanEdges in jni/src/gc/SEA/SubPolygon.cpp:69

對應的代碼行:


POD類型對象拷貝調用的是memcpy。看到這個結果我們懷疑是某種極端的面數據導致了引擎的crash,於是乎大家雄心勃勃,一起討論了一個方案:寫一個benchmark程序在內存中處理全國所有城市數據。但是跑了好幾天也沒辦法復現,時間一點點過去,大家的意志力逐漸被消磨殆盡,crash日誌還是越漲越多。

最終一個經驗豐富的高工最終了問題的可能原因:SubPolygon沒有特殊處理頂點爲0的情況。原因是生成瓦片中,多邊形使用軟件方法裁剪時可能會生成頂點爲0的多邊形,然後進行繪製。

舉個例子:int* ptr = new int[0]; ptr返回指針是不確定的,可能爲空也可能不爲空。malloc(0)返回的指針除了可以傳入free函數之外不建議有其他操作,直接訪問內存會出現隨機性的結果。Linux上malloc(0)行爲:http://www.cnblogs.com/xiaowenhu/p/3222709.html

教訓:代碼中增加一些合法性判斷代碼,覆蓋各種邊界處邏輯。它們絕對不會是Dead Code,因爲它們什麼時候起作用,你很難想象到或者根本沒辦法預料到。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章