坑爹的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方法也是最常用的!這個確實是作者在給我們挖坑哈!

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




發佈了86 篇原創文章 · 獲贊 854 · 訪問量 116萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章