軟件開發最關心的三個指標:性能、內存、程序穩定性三方面。本文總結一下最近項目掃尾工作中的一些遭遇:
使用正確的哈希函數
道路的路況繪製,道路的顏色由三個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,因爲它們什麼時候起作用,你很難想象到或者根本沒辦法預料到。