慎用頻繁小塊記憶體申請,讓程式健步如飛
最近碰到一個應用,為一塊已經載入記憶體的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
爽歪歪,飛一般的感覺酷斃了!!!!!!!!!!!!!!!!!!!!!!!!!