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