最近在做一個性能要求較高的項目,有個服務器需要處理每秒2萬個udp包,每個包內有40個元素(當然這是高峯期)。服務器需要一個鏈表,算法中有個邏輯要把每個元素添加到鏈表末尾(只是這個元素對象的指針,不存在對象複製的問題),再從鏈表中把這些元素取出(另一個時間點)。就是一個單線程在做這件事。
既然邏輯這麼簡單,我自然選用了C++的標準STL容器List(Linux GNU,sgi的實現),想來如此簡單的事情,不過是一次末尾插入,一次頭部取出而已,就用STL的List容器吧。沒有想到這是痛苦的開始。我預想中每秒處理80萬元素應該是很輕鬆寫意的,沒想到每秒一千個包時服務器就頂不住了,處理算法的線程佔用CPU達到百分之百,大量的包得不到及時處理而超時。由於算法較爲複雜,定位這問題耗了不少時間,終於感覺到LIST容器似乎有嚴重性能問題。
於是乾脆自己寫了個簡單的鏈表,替代了STL的容器後性能有了極大的提升。爲此我特意寫了個簡單的程序,大致模仿我算法中的場景,程序流程如下:
每3秒中向鏈表中插入N個元素(指針),再把這N個元素從鏈表中取出釋放。之後查看耗時t,如果t小於3秒,就sleep(3-t)秒,並打印出睡眠的時間。
在我的測試機上,出現了差異很大的測試結果,大約每3秒測試2萬個元素時,使用STL LIST的壓力程序導致CPU已經達到70%了,而使用自己寫的簡單鏈表幾乎沒有感覺。直到每3秒測試2000萬個元素時,才導致CPU佔用80%。結果有一千倍的差距!這裏沒有對象的複製,我插入鏈表的都只是指針而已!
(下面附測試程序,這裏只是對比兩種list的性能,機器的參數並不重要。請大家注意71行代碼)
#include <list>
#include <sys/time.h>
#include <iostream>
using namespace std;
//待測試的對象,鏈表中的每個元素就是對象A的指針
class A {};
//每3秒鐘插入鏈表末尾/從鏈表首部取出的元素個數
int testPressureNum = 40000;
//測試的STL鏈表
list<A*> testList;
//自己寫的鏈表
typedef struct
{
A* p;
void* prev;
void* next;
} SelfListElement;
SelfListElement* myListHead;
SelfListElement* myListTail;
int myListSize;
//向自己寫的鏈表首部添加元素
bool add(A* packet)
{
SelfListElement* ele = new SelfListElement;
ele->p = packet;
myListSize++;
if (myListHead == NULL)
{
myListHead = myListTail = ele;
ele->prev = NULL;
ele->next = NULL;
return true;
}
ele->next = myListHead;
myListHead->prev = ele;
ele->prev = NULL;
myListHead = ele;
return true;
}
// 從自己寫的鏈表尾部取出元素
SelfListElement* get()
{
if (myListTail == NULL)
return NULL;
myListSize--;
SelfListElement* p = myListTail;
if (myListTail->prev == NULL)
{
myListHead = myListTail = NULL;
}
else
{
myListTail = (SelfListElement*)myListTail->prev;
myListTail->next = NULL;
}
return p;
}
//從STL鏈表中取出元素並刪除
void testDelete1()
{
while (testList.size() > 0)//這行語句有嚴重性能問題,size的複雜度不是常量級,而是O(N),請注意!就是這裏跳坑裏去了
{
A* p = testList.back();
testList.pop_back();
delete p;
p = NULL;
}
}
//從簡單鏈表中取出元素並刪除
void testDelete2()
{
do {
SelfListElement* packet = myListTail;
if (packet == NULL)
break;
packet = get();
delete packet->p;
delete packet;
packet = NULL;
} while (true);
}
//向Stl鏈表中添加元素
void testAdd1()
{
for (int i = 0; i < testPressureNum; i++)
{
A* p = new A();
testList.push_front(p);
}
}
//向簡單鏈表中添加元素
void testAdd2()
{
for (int i = 0; i < testPressureNum; i++)
{
A* p = new A();
add(p);
}
}
void printUsage(int argc, char**argv)
{
cout<<"usage: "<<argv[0]<<" [1|2] [oneRoundPressueNum]"<<endl
<<"1 means STL, 2 means simple list\noneRoundPressueNum means in 3 seconds how many elements add/del in list"<<endl;
}
int main(int argc, char** argv)
{
//爲方便測試可使用2個參數
if (argc < 2)
{
printUsage(argc, argv);
return -1;
}
int type = atoi(argv[1]);
if (type != 1 && type != 2)
{
printUsage(argc, argv);
return -2;
}
if (argc >= 2)
testPressureNum = atoi(argv[2]);
cout<<"every 3 seconds add/del element number is "<<testPressureNum<<endl;
struct timeval time1, time2;
gettimeofday(&time1, NULL);
while (true)
{
gettimeofday(&time1, NULL);
if (type == 1)
{
testAdd1();
cout<<"stl list has "<<testList.size()<<" elements"<<endl;
}
else
{
testAdd2();
cout<<"my list has "<<myListSize<<" elements"<<endl;
}
//每3秒一個週期
gettimeofday(&time2, NULL);
unsigned long interval = 1000000*(time2.tv_sec-time1.tv_sec)+
time2.tv_usec-time1.tv_usec;
if (interval < 3000000)
{
cout<<"after add sleep "<<3000000-interval<<" usec"<<endl;
usleep(3000000-interval);
}
else
cout<<"add cost time too much"<<interval<<endl;
gettimeofday(&time1, NULL);
if (type == 1)
{
testDelete1();
cout<<"stl list has "<<testList.size()<<" elements"<<endl;
}
else
{
testDelete2();
cout<<"my list has "<<myListSize<<" elements"<<endl;
}
//每3秒一個週期
gettimeofday(&time2, NULL);
interval = 1000000*(time2.tv_sec-time1.tv_sec)+
time2.tv_usec-time1.tv_usec;
if (interval < 3000000)
{
cout<<"after delete sleep "<<3000000-interval<<" usec"<<endl;
usleep(3000000-interval);
}
else
cout<<"delete cost time too much"<<interval<<endl;
}
return 0;
}
一千倍的性能差距太誇張了。究竟是什麼原因導致STL性能這麼差呢?之前對在一些性能要求高的場景下我沒怎麼用過STL容器,對它還不夠熟悉。這篇博客發出後,陳碩幫忙指出原來是第71行的size()方法出了問題! 將size()方法改爲 empty()方法後,list性能有了大幅度提高,當然與上面自己寫的鏈表相比還有差距,大概自寫鏈表性能比STL的list還要高出70%左右!
我很好奇究竟size()方法幹了些什麼?看看它的實現!(STL的代碼我下的是sgi 3.3版本)
size_type size() const {
size_type __result = 0;
distance(begin(), end(), __result);
return __result;
}
原來這個size()方法並不像自己寫的鏈表那樣,用一個變量來存儲着鏈表的長度,而是去調用了distance方法來獲取長度。那麼這個distance方法又做了些什麼呢?
template <class _InputIterator, class _Distance>
inline void distance(_InputIterator __first,
_InputIterator __last, _Distance& __n)
{
__STL_REQUIRES(_InputIterator, _InputIterator);
__distance(__first, __last, __n, iterator_category(__first));
}
又封了一層__distance,看看它又做了什麼?
template <class _InputIterator>
inline typename iterator_traits<_InputIterator>::difference_type
__distance(_InputIterator __first, _InputIterator __last, input_iterator_tag)
{
typename iterator_traits<_InputIterator>::difference_type __n = 0;
while (__first != __last) {
++__first; ++__n;
}
return __n;
}
原來是遍歷!爲什麼獲得鏈表長度必須要遍歷全部的鏈表元素才能獲得,而不是用一個變量來表示呢?sgi設計list的思路何以如此與衆不同呢(話說微軟的STL實現就沒有這個SIZE方法的效率問題)?
看看作者自己的解釋:http://home.roadrunner.com/~hinnant/On_list_size.html
開篇點題,原來作者是爲了
splice(iterator position, list& x, iterator first, iterator last);
方法所取的折衷,爲了它的實現而把size方法設計成了O(N)。
splice方法就是爲了把鏈表A中的一些元素直接串聯到鏈表B中,如果size()設計爲O(1)複雜度,那麼做splice時就需要遍歷first和last間的長度(然後把鏈表A保存的鏈表長度減去first和last(待移動的元素)之間的長度)!於是作者考慮到size方法設計爲O(N),就無需在splice方法執行時做遍歷了!
看看splice的實現:
void splice(iterator __position, list&, iterator __first, iterator __last) {
if (__first != __last)
this->transfer(__position, __first, __last);
}
再看看transfer幹了些什麼:
void transfer(iterator __position, iterator __first, iterator __last) {
if (__position != __last) {
// Remove [first, last) from its old position.
__last._M_node->_M_prev->_M_next = __position._M_node;
__first._M_node->_M_prev->_M_next = __last._M_node;
__position._M_node->_M_prev->_M_next = __first._M_node;
// Splice [first, last) into its new position.
_List_node_base* __tmp = __position._M_node->_M_prev;
__position._M_node->_M_prev = __last._M_node->_M_prev;
__last._M_node->_M_prev = __first._M_node->_M_prev;
__first._M_node->_M_prev = __tmp;
}
}
作者確實是考慮splice執行時,不用再做遍歷,而是僅僅移動幾個指針就可以了,因此犧牲了size的效率!
怎麼評價這種設計呢?作者的出發點是好的,但是,畢竟絕大多數程序員都會潛意識認爲 size方法的複雜度是常量級的,同時size方法也是最常用的!這個確實是作者在給我們挖坑哈!
這個例子真是告訴我們,必須謹慎使用第三方軟件,特別是對它有較高的要求時,務必對將要使用它的所有方法都有足夠的瞭解,不是滿足於它能做什麼,還必須要知道它怎麼做到的,否則,還是老老實實用自己熟悉的工具吧。