1. 程式人生 > >坑爹的list容器size方法--為了splice居然把複雜度設計為O(N)?

坑爹的list容器size方法--為了splice居然把複雜度設計為O(N)?

 最近在做一個性能要求較高的專案,有個伺服器需要處理每秒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方法也是最常用的!這個確實是作者在給我們挖坑哈!

這個例子真是告訴我們,必須謹慎使用第三方軟體,特別是對它有較高的要求時,務必對將要使用它的所有方法都有足夠的瞭解,不是滿足於它能做什麼,還必須要知道它怎麼做到的,否則,還是老老實實用自己熟悉的工具吧。