完成端口通訊服務器(IOCP Socket Server)設計
(三)不要迷信API(單鏈表的另一種算法)
Copyright © 2009 代碼客(盧益貴)版權所有
QQ:48092788 源碼博客:http://blog.csdn.net/guestcode
用這個標題可能會牽強了點。只是因爲在性能優化中遇到這樣的事情,因此用來做標題而已,由此通過一個小事拋出本文介紹的內容:單鏈表的另一種算法。我本人也做了個ARM的OS內核,雖然及其簡單,但是OS的機理大相徑庭。操作系統要做到線程同步,需要進入中斷(應用程序同步一般是軟件中斷)才能做到,在進入和退出這兩個環節付出的代價相當昂貴的(事實上操作系統的分時處理機制(時鐘中斷)也在付出昂貴的代價,但那是不可避免的)。
有時候瞭解的越多,考慮的因素越複雜,做出的結果會越錯誤。正如上面所說的,線程同步需要使用臨界會消耗巨大時間(比起非使用臨界而言)。在做內存管理的釋放函數的時候,有牛人告訴我,可以使用獨立線程來處理釋放工作,在釋放函數裏面給它發個消息就得了,比如使用PostThreadMessage一個函數就搞定了,又避免了使用臨界。剛開始我一聽,也感覺真的是很簡單,避免了釋放的時候釋放函數做太多工作而造成堵塞。但隨後一想,我向他提出了幾個問題:“線程什麼時候讀取消息?隔1毫秒?隔10毫秒?會不會因爲延時造成內存耗盡的錯象?”他又給我推薦了SetEvent,用它及時告知Thread去處理釋放工作。感覺好像“非常有道理”。
不過我沒有立即去這麼做,而是做了幾個代碼段去測試(實際對於一個資深的程序員來說根本不需要測試他們是否使用同步機制,本人只想:既然都是使用臨界如果那種方式效率高的話還是可取的):
第一種情形:
dwTickCount = GetTickCount();
for(i = 0; i < 10000000; i++)
{
EnterCriticalSection(&csSection);
//在這裏處理釋放工作
LeaveCriticalSection(&csSection);
}
第二種情形:
dwTickCount = GetTickCount();
for(i = 0; i < 10000000; i++)
{
EnterCriticalSection(&csSection);
//在這裏把釋放的地址放入處理隊列
LeaveCriticalSection(&csSection);
//告訴處理線程,你該工作了
SetEvent(hEvent);
}
dwTickCount = GetTickCount() - dwTickCount;
第三種情形:
dwTickCount = GetTickCount();
for(i = 0; i < 10000000; i++)
{
PostThreadMessage(dwThreadID, WM_USER, 0, 0);
//告訴處理線程,你該工作了
SetEvent(hEvent);
}
dwTickCount = GetTickCount() - dwTickCount;
MSG Msg;
while(PeekMessage(&Msg, 0, 0, 0, PM_REMOVE));
上面3種情形都有兩個線程互相作用,測試結果是:
1、dwTickCount = 4281
2、dwTickCount = 17563
4、dwTickCount = 37297
儘管我的機器主板修了幾次,巨慢無比,但在環境一樣的前提下得出這麼懸殊得結果,已經可以排除2、3種情形了(當然如果第1種情形中的釋放處理工作消耗的時間遠大於“2、3種情形減輕1種情形”的話就另當別論了;另外第3種情形連續投遞這麼多消息或多或少內核處理也會耗時)。與其這麼折騰,還不如在自己的釋放代碼上下功能呢(源碼可以看上一篇文章)。
通過這個小事,說明了一個問題,有時候在開發過程種,我們已經無意中過多的信任了API,總以爲看到的代碼僅是一行API而已,比起(顯示式)使用臨界來說效率是優秀的(這個錯誤在以往本人也曾犯過,是在編碼的時候一時興起導致“一念之差”的大意行爲)。在多線程環境下,部分API是需要進入“臨界”這樣的機制來同步的,諸如:SendMessage和PostMessage,GetMessge和PeekMessage。
我們都希望想系統能夠提供效率更高的API,來滿足我們對服務器性能的需要,比如Windows提供了完成端口功能,是不是系統還有更有效的方式?!更有人想直接操作物理內存,甚至有人說:如果在驅動層上下功夫是不是效率更高?毫不隱諱的講:我也有過這樣大膽的想法。但在現在的條件下,我們指望API似乎已經沒有多大希望。與其……不如優化我們的算法。下面我就介紹一個單向鏈表的另一種提高效率的算法。(由於本人信息閉塞,這個算法不知道是否已經有人發佈過目前我尚未得知。)
在做IOCP服務器的時候,爲了提高效率,我們採用內存池和連接池的方法以避免頻繁向系統索要和歸還內存造成系統內存更多的碎片(這種方式效率也是低的)。這個方法好是好,但內存池和連接池一般也都使用了臨界來同步,同時對於一個數據區一般也習慣使用一個臨界變量來達到同步目的,如果併發性高的話這種同步就會造成堵塞。能不能再提高一點效率以此來降低堵塞的可能性?
假如我們使用兩個臨界變量來同步一個單向鏈表會不會把堵塞機率降低一半呢?方法是這樣的:單向鏈表採用後進後出的方式,一個臨界負責鏈表頭的同步,另一個臨界負責鏈表尾的同步,但這樣的前提是要保證這個鏈表不爲空。
下面是根據上面設想以後的優化算法(後進後出):
PGIO_BUF GIodt_AllocGBuf(void)
/*說明:分配一個內存塊GBuf,提供給業務層
**輸入:無
**輸出:內存塊GIoData地址*/
{
//鎖鏈表頭
EnterCriticalSection(&GIoDataPoolHeadSection);
//確保鏈表至少有一個節點,pGIoDataPoolHead不爲空,除非不初始化
//假如一個設計者考慮正常和異常情況能保證內存用之不盡的話,下面這個判斷是多餘,
//在以往的設計中本人就曾這麼大膽過。
if(pGIoDataPoolHead->pNext)//和常規算法if(pGIoDataPoolHead)相比並沒有多大開支
{
PGIO_BUF Result;
Result = (PGIO_BUF)pGIoDataPoolHead;
pGIoDataPoolHead = pGIoDataPoolHead->pNext;
dwGIoDataPoolUsedCount++;
LeaveCriticalSection(&GIoDataPoolHeadSection);
//爲什麼會這樣返回:(char *)Result + sizeof(GIO_DATA_INFO),
//這也是優化算法的一種,將在以後介紹
return((char *)Result + sizeof(GIO_DATA_INFO));
}else
{
LeaveCriticalSection(&GIoDataPoolHeadSection);
return(NULL);
}
}
void GIodt_FreeGBuf(PGIO_BUF pGIoBuf)
/*說明:業務層歸還一個內存塊GBuf
**輸入:內存塊GIoData地址
**輸出:無*/
{
//鎖鏈表尾
EnterCriticalSection(&GIoDataPoolTailSection);
pGIoBuf = (char *)pGIoBuf - sizeof(GIO_DATA_INFO);
((PGIO_DATA)pGIoBuf)->pNext = NULL;
pGIoDataPoolTail->pNext = (PGIO_DATA)pGIoBuf;
pGIoDataPoolTail = (PGIO_DATA)pGIoBuf;
dwGIoDataPoolUsedCount--;
LeaveCriticalSection(&GIoDataPoolTailSection);
}
以下是常規的算法(先進先出):
PGIO_BUF GIodt_AllocGBuf(void)
/*說明:分配一個內存塊GIoBuf,提供業務層
**輸入:無
**輸出:內存塊GIoData地址*/
{
PGIO_BUF Result;
//鎖鏈表
EnterCriticalSection(&GIoDataPoolSection);
Result = (PGIO_BUF)pGIoDataPoolHead;
if(pGIoDataPoolHead)
{
pGIoDataPoolHead = pGIoDataPoolHead->pNext;
dwGIoDataPoolUsedCount++;
}
LeaveCriticalSection(&GIoDataPoolSection);
return((char *)Result + sizeof(GIO_DATA_INFO));
}
void GIodt_FreeGBuf(PGIO_BUF pGIoBuf)
/*說明:業務層歸還一個內存塊GIoBuf
**輸入:內存塊GIoData地址
**輸出:無*/
{
//鎖鏈表
EnterCriticalSection(&GIoDataPoolSection);
pGIoBuf = (char *)pGIoBuf - sizeof(GIO_DATA_INFO);
((PGIO_DATA)pGIoBuf)->pNext = pGIoDataPoolHead;
pGIoDataPoolHead = (PGIO_DATA)pGIoBuf;
dwGIoDataPoolUsedCount--;
LeaveCriticalSection(&GIoDataPoolSection);
}
細心的讀者應該會發現,爲什麼優化算法是這樣:
if(pGIoDataPoolHead->pNext)
{
PGIO_BUF Result;
…
LeaveCriticalSection(&GIoDataPoolHeadSection);
return((char *)Result + sizeof(GIO_DATA_INFO));
}else
{
LeaveCriticalSection(&GIoDataPoolHeadSection);
return(NULL);
}
和這樣的算法有什麼區別(這樣代碼量會更少而又簡潔):
PGIO_BUF Result;
if(pGIoDataPoolHead->pNext)
{
…
}else
Result = NULL;
LeaveCriticalSection(&GIoDataPoolHeadSection);
return(NULL);
這個疑問,只有看了彙編後的代碼才能解決了:前面的方法少執行了一兩句彙編代碼(現在僅談算法效率,以後有時間再談代碼效率)。
上述單向鏈表的算法僅爲個人搓見,希望能得到牛人指點迷津,使得算法更加有效率。
.....