慎用頻繁小塊內存申請,讓程序健步如飛

最近碰到一個應用,爲一塊已經載入內存的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
爽歪歪,飛一般的感覺酷斃了!!!!!!!!!!!!!!!!!!!!!!!!!

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