1. 程式人生 > >慎用頻繁小塊記憶體申請,讓程式健步如飛

慎用頻繁小塊記憶體申請,讓程式健步如飛

最近碰到一個應用,為一塊已經載入記憶體的Tab檔案生成一個動態陣列用於建立單元格資料索引表。

當然這也算是C vs C++的一個典型例子吧。
由於事先不知道Tab檔案的行數和列數,無法預先生成動態陣列。

方案1:
首先想到的是遍歷整個檔案,用一個臨時map記錄每個單元格的索引資訊,並計算出表格的行數和列數。
然後申請根據行數和列數申請一個合適大小的動態陣列,並將臨時map中的資料搬到動態陣列中,這樣只需要遍歷一次檔案,完美。
但是測試發現,載入約200個Tab檔案解析共6萬行資料,耗時高達15秒,F5模式更是離譜,居然高達150秒。
地球人已經無法忍受了,分析原因,臨時map底層隱式涉及到了太多的小塊記憶體申請操作,該方案理想很高遠,現實太骨感,pass。

方案2:
由於檔案資料已經在記憶體,如果遍歷檔案2次,第一次計算出檔案的行數和列數,第二次遍歷生成動態陣列並計算單元格索引。
看起來很”挫”的方案,效果如何呢?perfect,同樣的資料,解析耗時0.125秒,F5模式下只有0.124秒。
另外,考慮到兩次遍歷的相似性,將遍歷部分抽象成新函式,讓本來很挫的設計看起來很優雅。

別的不說了,晒一下兩個方案的程式碼和測試結果。

方案1:
//用臨時map這個方案太慢了,作為大量隱式小塊記憶體申請的反面教材留在這做參考吧

int TabFile::CreateTabOffset()
{
    clock_t ts = clock();

    if
(!m_pMemory || !m_uMemorySize) return true; typedef unsigned int UINTT; typedef std::map<UINTT, TABOFFSET> COL_MAP; typedef std::map<UINTT, COL_MAP> ROW_COL_MAP; ROW_COL_MAP offset_map; unsigned char *pBuff = m_pMemory; unsigned int nOffset = 0; unsigned
int nSize = m_uMemorySize; int nMaxCol = 0; int nRowIdx = 0; for (nRowIdx = 0; nOffset < nSize; ) //讀取所有行 { int nColIdx = 0; for (nColIdx = 0; nOffset < nSize;) //讀取一行所有列 { TABOFFSET tmp_offset; tmp_offset.dwOffset = nOffset; unsigned int nLen = 0; //讀取一個單元格的內容 while(*pBuff != 0x09 && *pBuff != 0x0d && *pBuff != 0x0a && nOffset < nSize) { pBuff++; nOffset++; nLen++; } tmp_offset.dwLength = nLen; offset_map[nRowIdx][nColIdx] = tmp_offset; if (nOffset < nSize) {//如果是因為讀到檔案結束退出while迴圈,下面的chLastChar初始化就訪問越界了 //所以要先做一次越界檢查 ++nRowIdx;//已經讀取到了內容,說明這一行不是空行,行號+1 break; } const char chLastChar = *pBuff; // 0x09或0x0d或0x0a(linux)跳過 pBuff++; nOffset++; //反正沒用到*pBuff,先跳過分隔符再來判斷越界沒有 //防止以0x09結尾沒有正確記錄行數的情況 if (!(nOffset < nSize)) {//已經到檔案末尾了 ++nRowIdx;//已經讀取到了內容,說明這一行不是空行,行號+1 break; } if (chLastChar == 0x0d || chLastChar == 0x0a) {// 0x0d或0x0a(linux)跳過 if (*(pBuff - 1) == 0x0d && *pBuff == 0x0a) {//跳過行尾 pBuff++; nOffset++; } ++nRowIdx;//遇到行結束符,行號+1 break; } ++nColIdx;//列號+1 } if (nColIdx > nMaxCol) {//記錄最大行的列數 nMaxCol = nColIdx; } } m_Height = nRowIdx; m_Width = nMaxCol + 1; m_pOffsetTable = (TABOFFSET*)malloc(m_Width * m_Height * sizeof(TABOFFSET)); if (m_pOffsetTable == NULL) return false; memset(m_pOffsetTable, 0, m_Width * m_Height * sizeof(TABOFFSET)); ROW_COL_MAP::const_iterator it1 = offset_map.begin(); for (; it1 != offset_map.end(); ++it1) { int nRow = it1->first; COL_MAP::const_iterator it2 = it1->second.begin(); for (; it2 != it1->second.end(); ++it2) { int nCol = it2->first; TABOFFSET* pOff = m_pOffsetTable + (m_Width * nRow + nCol); ::memcpy(pOff, &it2->second, sizeof(TABOFFSET)); } } offset_map.clear();//無法測試到map的析構,在這裡加上clear模擬損耗 //////////////////////////////////////////////////統計消耗 static double dtotal = 0; static int nrowtotal = 0; static int nfiletotal = 0; clock_t te = clock(); double dt = double(te - ts) / CLK_TCK; dtotal += dt; nrowtotal += m_Height; printf("Parse no[%d] tab file ok, row=%d/%d, t=%f/%fSec\n" , ++nfiletotal, m_Height, nrowtotal, dt, dtotal); return true; }

測試結果:
雙擊執行:
Parse no[168] tab file ok, row=100/59843, t=0.000000/15.517000Sec
F5執行:
Parse no[168] tab file ok, row=100/59843, t=0.015000/155.917005Sec
哇哦,慢得一塌糊塗,受不鳥了!!!!!!!!!!!!!!!!!!!!!!!!!

方案2:
//還是這個方案靠譜,雖然有2次檔案遍歷,但是沒有任何小塊記憶體申請,速度快到難以想象

int TabFile::CreateTabOffset()
{
    clock_t ts = clock();

    if (!m_pMemory || !m_uMemorySize)
        return true;

    BOOL bRet = TRUE;
    bRet &= _ParseTabFile(FALSE);
    bRet &= _ParseTabFile(TRUE);

    //////////////////////////////////////////////////統計消耗
    static double dtotal = 0;
    static int nrowtotal = 0;
    static int nfiletotal = 0;
    clock_t te = clock();
    double dt = double(te - ts) / CLK_TCK;
    dtotal += dt;
    nrowtotal += m_Height;
    printf("Parse no[%d] tab file ok, row=%d/%d, t=%f/%fSec\n"
        , ++nfiletotal, m_Height, nrowtotal, dt, dtotal);

    return bRet;
}

int TabFile::_ParseTabFile(BOOL bGenerateOffset)
{
    if (!m_pMemory || !m_uMemorySize)
        return true;

    const unsigned int nSize = m_uMemorySize;

    unsigned char   *pBuff = m_pMemory;
    unsigned int nOffset = 0;

    if (bGenerateOffset)
    {
        if (!m_Height || !m_Width)
        {
            printf("****Parse tab file fail!****\n");
            return FALSE;
        }
        m_pOffsetTable = (TABOFFSET*)malloc(m_Width * m_Height * sizeof(TABOFFSET));
        if (m_pOffsetTable == NULL)     return FALSE;
        memset(m_pOffsetTable, 0, m_Width * m_Height * sizeof(TABOFFSET));
    }

    int nMaxCol = 0;
    int nRowIdx = 0;
    for (nRowIdx = 0; nOffset < nSize; ) //讀取所有行
    {
        int nColIdx = 0;
        for (nColIdx = 0; nOffset < nSize;) //讀取一行所有列
        {
            const unsigned int nFieldBeginOffset = nOffset;
            unsigned int nLen = 0;
            //讀取一個單元格的內容
            while(*pBuff != 0x09 && *pBuff != 0x0d && *pBuff != 0x0a && nOffset < nSize) 
            {
                pBuff++;
                nOffset++;
                nLen++;
            }

            if (bGenerateOffset)
            {
                TABOFFSET* pOff = m_pOffsetTable + (m_Width * nRowIdx + nColIdx);
                pOff->dwLength = nLen;
                pOff->dwOffset = nFieldBeginOffset;
            }

            if (nOffset < nSize)
            {//如果是因為讀到檔案結束退出while迴圈,下面的chLastChar初始化就訪問越界了 
            //所以要先做一次越界檢查
                ++nRowIdx;//已經讀取到了內容,說明這一行不是空行,行號+1
                break;
            }

            const char chLastChar = *pBuff;
            // 0x09或0x0d或0x0a(linux)跳過
            pBuff++;
            nOffset++;

            //反正沒用到*pBuff,先跳過分隔符再來判斷越界沒有
            //防止以0x09結尾沒有正確記錄行數的情況
            if (!(nOffset < nSize))
            {//已經到檔案末尾了
                ++nRowIdx;//已經讀取到了內容,說明這一行不是空行,行號+1
                break;
            }

            if (chLastChar == 0x0d || chLastChar == 0x0a)
            {// 0x0d或0x0a(linux)跳過
                if (*(pBuff - 1) == 0x0d && *pBuff == 0x0a)
                {//跳過行尾
                    pBuff++;
                    nOffset++;
                }
                ++nRowIdx;//遇到行結束符,行號+1
                break;
            }
            ++nColIdx;//列號+1
        }

        if (nColIdx > nMaxCol)
        {//記錄最大行的列數
            nMaxCol = nColIdx;
        }
    }

    if (!bGenerateOffset)
    {
        m_Height = nRowIdx;
        m_Width = nMaxCol + 1;
    }
    return TRUE;
}

測試結果:
雙擊執行:
Parse no[168] tab file ok, row=100/59843, t=0.000000/0.125000Sec
F5執行:
Parse no[168] tab file ok, row=100/59843, t=0.000000/0.124000Sec
爽歪歪,飛一般的感覺酷斃了!!!!!!!!!!!!!!!!!!!!!!!!!