慎用频繁小块内存申请,让程序健步如飞

最近碰到一个应用,为一块已经载入内存的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
爽歪歪,飞一般的感觉酷毙了!!!!!!!!!!!!!!!!!!!!!!!!!

猜你喜欢

转载自blog.csdn.net/vipally/article/details/53148955