常见性能优化小技巧原理

时间:2021-08-08 06:11:52

一、多用有序数组+折半查找
金山卫士开源后立马招来各种批判,其中有一段批评金山卫士源码说太多if else而不用表驱动使得代码可读性不高,笔者看了下大致如下:

TCHAR  szFolderPath[MAX_PATH + 1] = {0}; 
// MichaelPeng: if else太多,应做成表驱动
if (0 == _tcsicmp(szVariable, _T("%Desktop%"))) {
::SHGetSpecialFolderPath(NULL, szFolderPath, CSIDL_DESKTOP, 0);
} else if (0 == _tcsicmp(szVariable, _T("%Internet%"))) {
::SHGetSpecialFolderPath(NULL, szFolderPath, CSIDL_INTERNET, 0);
} else if (0 == _tcsicmp(szVariable, _T("%Favorites%"))) {
::SHGetSpecialFolderPath(NULL, szFolderPath, CSIDL_FAVORITES, 0);
} else if (0 == _tcsicmp(szVariable, _T("%CommonDocuments%"))) {
::SHGetSpecialFolderPath(NULL, szFolderPath, CSIDL_COMMON_DOCUMENTS, 0);
}

.......

算上中间省去的代码这代码这一共有三十项比较,代码写成这样的确不雅。曾经在和朋友讨论提出使用map,通过以字符串大小做key,这样查找效率会提高很多。不过笔者认为最好的方法还是结构体数组,然后通过字符串大小规则排序,然后使用折半查找法保证效率和map一致(其实略高于红黑树结构的map,严格的说map的查找效率只是接近lgN,具体可以参考红黑树数据结构),这样做能保证查找效率的时候还能够节约内存,虽说这地方因为数据量小而体验不出明显节约内存。

为什么数组比map更节约内存?因为map是采用红黑树数据结构,每个节点都是一个单独的堆节点,考虑到每次申请内存时多余的堆数据结构和指向其他节点的指针,当节点数达到一定时候内存占用量就会表现越加明显。

其实可以做一个简单的实验,分别写两个程序测试;

void main()
{
char *p = new char[100000];
getchar();
}

void main()
{
for (int i = 0;i <100000;i++)
char *p = new char;
getchar();
}

上面两个程序运行起来你会从任务管理器发现后者占内存明显比前者大,其实是一样的道理。类似这种写法在开源的duilib里面也存在不少,对话框上空间创建时会根据各种名字判断来做相应的操作,很多比较也是不断的if else,但是这种方法并不是什么时候都适用的,考虑到数组增加元素的麻烦性,如果元素需要不断增加删除,那么map就是更好的选择,另外如果能预先知道元素的查找频率,得知极少数元素查找频率极高而大部分元素极少查询,那么用表驱动,将查找频率最高的元素放在首位按照无序遍历也是不错的选择。

笔者曾经开发一款安全软件,由于该软件启动时会搜素本机上可保护的软件列表,同时能监控进程启动时判断该程序是否需要被保护起来(主要是防止进程被打开,注入等操作),但是软件携带的保护列表数以千计,前人在开发过程中或许是当时软件数目较少于是没做排序直接放到vector中,然后每次有进程启动都要遍历数组而效率很低,但是按照自己的规则进行排序后以折半查找方式则很大幅度减少资源以至于启动时搜索可保护软件列表明显提高一个档次。

二、巧用哨兵元素

问题由来:对于一个有N个元素的无序数组a[n],判断其中是否有key这个值,写一个函数。

很多人拿到这个问题直接写出如下代码

BOOL GetIndexBkey(int a[],int nSize,int nKey)
{
for (int i = 0; i < nSize;i++)
{
if (nKey == a[i])
{
return TRUE;
}
}

return FALSE;
}

这么写其实也能达到目的,但是细细看来每一次循环的时候都会比较两次i < nSize和nKey == a[i]。事实上我们可以用一种很巧妙的方法减少一次比较,这样以来在数据量很大的时候效率就会较明显提升,如果这个函数调用相当频繁那么优化效果还是很明显,具体如下:

BOOL GetIndexBkey(int a[],int nSize,int nKey)
{
int nlastIndex = nSize-1;
//首先判断最后一个元素是不是关键字,如果是直接返回
if (a[nlastIndex] == nKey)
{
return TRUE;
}

//保存数组中最后一个元素,同时将最后一个元素赋值成key
int nSavelastValue = a[nlastIndex];
a[nlastIndex] = nKey;

int i = 0;
while (a[i]!=nKey)
{
i++;
}

//由于最后一个元素为key,所以上面的循环必定有出口
//当循环跳出后如果此时索引指向最后一个元素,说明查找失败
//反之成功
a[nlastIndex] = nSavelastValue;
if (i < nlastIndex)
{
return TRUE;
}

return FALSE;
}

通过以上写法可以讲比较次数降低到N+2,同时增加4次赋值与一次减法操作,比起之前2N次比较工作量明显小了许多,同时CPU执行的指令也会大幅度减少,效率自然就更高了。

 

三、多用参数引用化

相信读者一眼就能看出来以下代码效率上的区别,不加引用程序会为参数创造出一个临时对象,并且在函数退出时析构,而换成引用后则省去这一步骤。如果这样的函数调用太过频繁这种写法节省出来的性能开销还是相当可观,但是很多人的代码并没有太过注意这一点。

void SetFileInfo(ThreadSortData fileinfo)
{
ThreadSortData fileinfo1 = fileinfo;
}

void SetFileInfo1(const ThreadSortData &fileinfo)
{
ThreadSortData fileinfo1 = fileinfo;
}

其实对于这种简单的结构体这种引用化参数并没带来太多意义上的性能节省,但是对于下面这个例子相信只要试过的读者肯定记忆极深,看代码

void SetlistInfo(list<int> listTest)
{
return;
}

int _tmain(int argc, _TCHAR* argv[])
{
list<int> listtest;
for (int i = 0;i < 1000000;i++)
{
listtest.push_back(i);
}

SetlistInfo(listtest);
return 0;
}

笔者在VS2005 debug版本下测试,当程序执行到SetlistInfo内部时内存占用量立刻翻倍,而且由于大量拷贝操作使得进该函数时耗时间和外面的for循环一样多,但是一旦参数引用化后进入SetlistInfo函数简直毫无压力。也就是说进入该函数的时候程序也重新拷贝一份list,并且复制listtest内部值到这个临时对象,由于listtest元素量特别多以至于这个拷贝耗时间和空间几乎让人无法容忍,到这里相信大家都能明白参数引用化可以带来多大的性能节省。

 

四、迭代器自加的正确写法

关于STL的教程很多都提到过迭代器自加应该写成++iter而不是iter++,对于类似整形数据来说其实这样写根本没什么区别,但是对于迭代器来说效率上却区别很大。下面通过反汇编代码看看加号写在前面和后面的区别(以Visual C++ 2005 Debug版本为例):

list<ThreadSortData>::iterator iter = listtest.begin();
004136EE lea eax,[ebp-64h]
004136F1 push eax
004136F2 lea ecx,[ebp-44h]
004136F5 call std::list<ThreadSortData,std::allocator<ThreadSortData> >::begin (411276h)
004136FA mov byte ptr [ebp-4],2
iter++;
004136FE push 0
00413700 lea eax,[ebp-144h]
00413706 push eax
00413707 lea ecx,[ebp-64h]
0041370A call std::list<ThreadSortData,std::allocator<ThreadSortData> >::_Iterator<1>::operator++ (41122Bh)
0041370F lea ecx,[ebp-144h]
00413715 call std::list<ThreadSortData,std::allocator<ThreadSortData> >::_Iterator<1>::~_Iterator<1> (41155Fh)
++iter;
0041371A lea ecx,[ebp-64h]
0041371D call std::list<ThreadSortData,std::allocator<ThreadSortData> >::_Iterator<1>::operator++ (411389h)

而在++iter第一个call当中调用的是以下代码(部分简化并没有实际贴全):

_Myt_iter _Tmp = *this;
00413FFC mov eax,dword ptr [ebp-14h]
00413FFF push eax
00414000 lea ecx,[ebp-28h]
00414003 call std::list<ThreadSortData,std::allocator<ThreadSortData> >::_Iterator<1>::_Iterator<1> (41163Bh)
00414008 mov dword ptr [ebp-4],1
++*this;
0041400F mov ecx,dword ptr [ebp-14h]
00414012 call std::list<ThreadSortData,std::allocator<ThreadSortData> >::_Iterator<1>::operator++ (411389h)

相信即使是完全没看过反汇编的朋友此时也能明白上述代码创建了一个临时对象,之后再去下一个值,然后析构之前创建的临时对象。但是对于++iter来说只是在地址0041371D处调用了一个call,这个call跟踪进去发现其实代码如下:

++(*(_Mybase_iter *)this);
00413F73 mov ecx,dword ptr [this]
00413F76 call std::list<ThreadSortData,std::allocator<ThreadSortData> >::_Const_iterator<1>::operator++ (411050h)
return (*this);
00413F7B mov eax,dword ptr [this]

相对++写在后面,此时少了一步构造析构的操作,当然这部分操作带来的性能节省将会随着对象的构造析构的复杂性或者调用频率而产生不同的效果。

未完待续...