天天看點

WaitForSingleObject 與 EnterCriticalSection 性能比較

摘要

在 Microsoft Windows 平台上有幾種以原子方式鎖定代碼和資料的不同方法。此白皮書的主要目的是向開發人員簡要介紹 Windows 中進行鎖定的不同方法以及與這些鎖定有關的相應性能開銷。因為未來架構将是多核架構,是以此資訊非常适用。

簡介

多線程軟體應用對于提升英特爾核心架構的性能至關重要。鎖定代碼通常是多線程應用中運作最頻繁的代碼。确定要使用的鎖定方法與确定應用中并行處理一樣重要。此白皮書的主要目的是向開發人員簡要介紹 Windows 中進行鎖定的不同方法以及與這些鎖定有關的相應性能開銷。Window 的某些鎖定 API 可能會跳轉至作業系統核心。此白皮書将詳細說明跳轉至核心的 API 以及相應的加入條件。

使用兩種不同的鎖定核心說明表示不同粒度的鎖定的影響。第一種鎖定核心模拟鎖定和取消鎖定動态連結清單(通常用于通過記憶體管理器維護一個空閑清單)的場景。對于多線程應用,首先需要鎖定此清單,線程才能嘗試配置設定或釋放記憶體。第二種鎖定核心表示更加精細地鎖定,因為它僅擷取已獲得鎖定的線程的 ThreadID,更新全局變量,然後釋放鎖定。通過采用 1 至 64 的線程數,對高、低争用場景下不同鎖定的性能進行測試。每一線程擷取執行 10,000,000 次某一運算的鎖定,然後釋放此鎖定。對于這些實驗,Windows XP 作業系統計時從 10 毫秒更改為 1 毫秒。WaitForSingleObject 和 EnterCriticalSectionMicrosoft Windows 平台中兩種最常用的鎖定方法為 WaitForSingleObject 和 EnterCriticalSection。WaitForSingleObject 是一個過載 Microsoft API,可用于檢查和修改許多不同對象(如事件、作業、互斥體、程序、信号、線程或計時器)的狀态。WaitForSingleObject 的一個不足之處是它會始終擷取核心的鎖定,是以無論是否獲得鎖定,它都會進入特權模式 (環路 0)。此 API 還進入 Windows 核心,即使指定的逾時為 0,亦如此。此鎖定方法的另一不足之處在于,它一次隻能處理 64 個嘗試對某個對象進行鎖定的線程。WaitForSingleObject 的優點是它可以全局進行處理,這使得此 API 能夠用于程序間的同步。它還具有為作業系統提供鎖定對象資訊的優勢,進而可以實作公平性及優先級倒置。

通過對關鍵代碼段實施 EnterCriticalSection 和 LeaveCriticalSection API 調用,可以使用EnterCriticalSection。此 API 具有 WaitForSingleObject 所不具備的優點,因為隻有存在鎖定争用時,才會進入核心。如果不存在鎖定争用,則此 API 會擷取使用者空間鎖定,并且在未進入特權模式的情況下傳回。如果存在争用,則此 API 在核心中所采用的路徑将與 WaitForSingleObject 極其相似。在低争用的情況下,由于 EnterCriticalSection 不進入核心,是以鎖定開銷非常低。

不足之處是 EnterCriticalSection 無法進行全局處理,是以無法為線程擷取鎖定的順序提供任何保證。EnterCriticalSection 是一種阻塞調用,意味着隻有線程獲得對此關鍵區段的通路權限時,該調用才會傳回。Windows 引入了 TryEnterCriticalSection,TryEnterCriticalSection 是一種非阻塞調用,無論獲得鎖定與否都會立即傳回。此外,EnterCriticalSection 還允許開發人員使用自旋計數對關鍵區段進行初始化,在回退前線程會按此自旋計數嘗試擷取鎖定。通過使用 API InitializeCriticalSectionAndSpinCount,完成初始化。自旋計數可以在此調用中進行設定,也可以在系統資料庫中進行設定,以根據不同作業系統及其相應的線程量程對自旋進行更改。

如果存在鎖定争用,則 EnterCriticalSection 和 WaitForSingleObject 都會進入核心。如果實作程度過高,從使用者模式到特權模式的轉換開銷将會非常大。

EnterCriticalSection 和 WaitForSingleObject API 調用在對使用數千個周期的運算進行鎖定時,通常不會影響性能。在這些情況下,鎖定調用本身的開銷不會如此突出。會導緻性能降低的情況是粒度鎖定,獲得和釋放此鎖定要花費數百個周期。在這些情況下,使用使用者級别鎖定則非常有益。

為了說明在低争用的情況下 WaitForSingleObject 調用與 EnterCriticalSection 調用的開銷情況,我們分别在 1 個和 2 個線程上運作了記憶體管理鎖定核心。在低争用的情況下,存在加速比 (WaitForSingleObject_Time / EnterCriticalSection_Time) 大約為 5 倍的性能之差。在 2 個線程持續争用的情況下,使用 EnterCriticalSection 和使用 WaitForSingleObject 之間的差别最小。在低争用的情況下存在性能差距的原因如下:WaitForSingleObject 在每次調用時都進入核心,而 EnterCriticalSection 隻有當存在鎖定争用時,才進入核心。

線程數量EnterCriticalSection 時間(毫秒)WaitForSingleObject 時間(毫秒)加速比1 個線程(無争用)178191875.22 個線程(争用)53594581561.1

圖 1:顯示了在具有 1 個和 2 個線程的情況下,EnterCriticalSection 和 WaitForSingleObject 所對應的記憶體管理核心情況。EnterCriticalSection 在 1 個線程(無争用)的情況下速度較快,因為如果獲得鎖定,EnterCriticalSection 不會跳轉至核心(特權模式)。

下圖 2 說明了 EnterCriticalSection 和 WaitForSingleObject 在高争用時的開銷情況,所采用的線程數介于 1 至 64 之間。在本實驗中,我們在向動态連結清單中推入值和從中彈出值的同時,鎖定和取消鎖定該清單。目的是模拟記憶體配置設定器空閑清單,為了配置設定或釋放記憶體,需頻繁鎖定該清單。核心會主動根據環境切換争用鎖定的線程,是以在兩次實驗中,CPU 平均負載都是 22%。很明顯,在高争用情況下,利用 EnterCriticalSection 和 WaitForSingleObject 的開銷并無太大差别。

WaitForSingleObject 與 EnterCriticalSection 性能比較

圖 2:顯示了在具有 1 到 64 條線程的情況下,EnterCriticalSection 和 WaitForSingleObject 所對應的記憶體管理核心情況。在高争用情況下,與 WaitForSingleObject 和 EnterCriticalSection 所關聯的開銷并無太大差别。

利用英特爾® VTune 分析器通過基于事件的采樣來收集時鐘滴答事件,這對于确定 EnterCriticalSection 和 WaitForSingleObject 的争用情況将有所幫助。在進行該實驗之前,開發人員需確定已下載下傳了正确的核心符号。有關如何下載下傳核心符号的說明,可從 Microsoft 開發人員網絡 MSDN 上擷取。

在利用 EnterCriticalSection 時,如果核心(ntoskrnl.exe 或 ntkrnlpa.exe)中花費了時間,則表明發生了争用。不存在争用時,EnterCriticalSection 調用和 LeaveCriticalSection 調用會分别将大部分時間花費在 RtlEnterCriticalSection 函數和 RtlLeaveCriticalSection 函數的 ntdll.dll 中。NTDLL.DLL 是在處理器的環路 3(非特權模式)級别上運作的動态連結庫。此 NTDLL.DLL 庫包含許多某個應用使用的運作時庫(RTL)代碼,不應與作業系統核心相混淆。EnterCriticalSection 遇到争用時,它所采取的路徑将與 WaitForSingleObject 極其相似。通過檢視 hal.dll、ntdll.dll 和 ntkrnlpa.exe(或 ntoskrnl.exe)中包含的函數,便可能了解是否發生 EnterCriticalSection 高争用的情況,而無需了解這些核心函數的詳細資訊,如圖 3 中所示。

WaitForSingleObject 與 EnterCriticalSection 性能比較

圖 3: 顯示了在發生 EnterCriticalSection 和 WaitForSingleObject 高争用的情況下,Windows 作業系統核心、ntdll.dll 和 hal.dll 中的熱門函數情況。

WaitForSingleObject 将始終跳轉至 Windows 作業系統核心,但仍可以确定是否發生了針對利用此調用的鎖定的高争用。當鎖定發生高争用時,WaitForSingleObject 在核心、ntdll.dll 和 hal.dll 中将分别采取不同的路徑。這些路徑與因線程無法獲得所需的鎖定而導緻核心根據環境切換線程有關。特别是 KiDispatchInterrupt(作業系統核心)、ZwYieldExecution(作業系統核心)、KiDispatchInterrupt(作業系統核心)、HalRequestlpi (hal.dll) 和 HalClearSoftwareInterrupt (hal.dll) 等都是能夠很好地監視核心内 WaitForSingleObject 或 EnterCriticalSection 争用的函數。

WaitForSingleObject 與 EnterCriticalSection 性能比較

圖 4: 顯示了在 WaitForSingleObject 調用的無争用和高争用情況下,Windows 作業系統核心、ntdll.dll 和 hal.dll 中的熱門函數情況。

如果存在鎖定争用,EnterCriticalSection 和 WaitForSingleObject 都将進入核心,對于操作粒度性較高的鎖定和高争用的鎖定,使用者級鎖定是最佳選擇。使用者級原子鎖定使用者級鎖定涉及利用處理器的原子操作指令以原子方式更新記憶體空間。原子操作指令涉及利用指令的鎖定字首,并将目标操作數指定給某個記憶體位址。在目前的英特爾處理器上,使用鎖定字首以原子方式運作以下指令:ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD 和 XCHG。在跳轉至核心之前,EnterCriticalSection 會利用原子操作指令嘗試擷取使用者空間鎖定。大多數指令必須明确使用鎖定字首,但 xchg 和 cmpxchg 指令例外,如果這兩個指令中包含記憶體位址,則暗示着鎖定字首的存在。

在英特爾 486 處理器時代,鎖定字首用于在總線上聲明一個鎖定,這通常會帶來較大的性能損失。從英特爾高能奔騰架構開始,總線鎖定已轉變為緩存鎖定。在大多數現代架構中,如果鎖定駐留在無法緩存的記憶體中,或鎖定超出了分割緩存線的緩存線邊界,則仍會在總線上聲明鎖定。這兩種情況都不大可能出現,是以大多數鎖定字首都将轉變為開銷低廉的緩存鎖定。

圖 3 包含一個采用程式集中的一些行編寫的簡單鎖定,以說明利用帶有鎖定字首的原子操作指令擷取鎖定的方法。在本例中,代碼僅僅測試指向的記憶體空間,以嘗試擷取某個鎖定。如果該記憶體空間包含一個 1,則表明另一個線程已獲得了該鎖定。如果記憶體空間為 0,則表明該鎖定是可用的。原子操作指令 xchg 用于嘗試與記憶體空間交換 1。如果在執行 xchg 指令後,eax 包含 0,則表明目前線程獲得了該鎖定。如果在執行原子操作指令 xchg 後,eax 包含 1,則表明另一個線程已獲得了該鎖定。注釋:edx 注冊包含鎖定變量的位址// 将 1 移至 eax 注冊mov eax, 1// 使用解除參考的 edx 中包含的值 xchg 1lock xchg DWORD PTR[edx],eax// 如果為零,則進行測試test eax,eax// 如果不是零,則跳過jne Target

圖 5: 顯示了一個簡單互斥鎖定的程式集。

要利用使用者空間鎖定(使用鎖定字首),不一定要編寫程式集。通過 InterlockedExchange、InterlockedIncrement、InterlockedDecrement、InterlockedCompareExchange 和 InterlockedExchangeAdd 等“Interlocked”API,Microsoft 提供了對用于同步的最常用原子操作指令的通路。這些 API 全部駐留在 kernel32.dll 中,ring3 級别(非特權模式)的應用程式将加載該 kernel32.dll。由于名稱易于混淆,許多開發人員誤以為 kernel32.dll 時間為核心時間,但此 dll 完全在 ring 3 級别(使用者模式)上運作。它确實是用作 API 跳轉至 Windows 核心的網關。Interlocked 函數根本不可能跳轉至 Windows 核心(特權模式)。

為說明利用 WaitForSingleObject 和使用者級鎖定等開銷高昂的鎖定的潛在開銷,使用簡單的具有回退功能的使用者級自旋等待鎖定運作記憶體管理核心。在高争用和低争用情況下,使用者級自旋等待的開銷要低幾個數量級。是以,使用者級鎖定便成為頻繁調用的粒度鎖定的首選。

WaitForSingleObject 與 EnterCriticalSection 性能比較

圖 6: 内聯使用者級自旋等待鎖定和 WaitForSingleObject 在記憶體管理鎖定核心上的開銷。使用者級原子鎖定上的自旋等待循環利用使用者模式鎖定的一個不足之處在于,核心不能用于提供自旋等待。這意味着,嘗試使用 xchg 或 cmpxchg 擷取鎖定的應用程式将必須轉而執行其他操作(如果未獲得鎖定)或針對該鎖定自旋。如果程式員想要以使用者模式實作自旋等待循環,則利用具有初始化自旋計數的 TryEnterCriticalSection 或使用諸如英特爾線程構模組化塊中提供的第三方鎖定庫将是最佳選擇。從理想的角度來看,當無法獲得鎖定時,使線程轉而執行其他操作(而非使用任何自旋等待)始終是最佳選擇。

為了更好地了解自旋等待循環的構造,針對鎖定核心建立了一個循環。在自旋等待循環中,首先使用原子操作指令嘗試擷取鎖定(通常使用具有鎖定字首的 cmpxchg 或 xchg)。如果未獲得鎖定,代碼将針對鎖定記憶體空間的讀取進行自旋,進而嘗試确定其他線程釋放該鎖定的時間。該讀取稱為“可變讀取”,因為它涉及 C 程式設計語言中的一種可變類型。可變類型具有幾條與之相關聯的規則,包括它們無法在系統資料庫中更新,以及必須利用它們的記憶體位址來操作。在下例中,我們首先嘗試擷取鎖定。在本例中,獲得的互斥鎖定将顯示 0 作為 xchg 操作的結果。

如果未獲得鎖定,則代碼将針對可變資料類型的髒讀進行自旋。然後會執行測試以确定該變量為 0 還是鎖定已打開。如果鎖定結果為 0,則線程将再次嘗試以原子方式擷取鎖定,并跳轉至要執行的受保護代碼。if (GETLOCK(lock) != 0){While (VARIOUS_CRITERIA) {_asm pause; // 自旋循環中的暫停指令if ((volatile int*)lock) == 0) // 針對髒讀(而非鎖定)進行自旋 {if (GETLOCK(lock)) == 0) {goto PROTECTED_CODE; } }}BACK_OFF_LOCK(lock); // 回退鎖定或執行其他操作}PROTECTED_CODE:

圖 6: 用于原子鎖定的有效自旋等待循環。請注意回退代碼以及針對可變讀取(而非鎖定)的自旋。通過自旋等待循環回退鎖定由于以下幾個原因,利用使用者空間自旋等待循環是很危險的。作業系統無法了解到處理器在自旋循環中并未執行有用的操作。是以它會允許線程繼續自旋,直至用盡其量程。量程是指配置設定給處理器上運作的每個線程的時間片。此問題線上程量程較高的 Windows 伺服器平台上顯得尤為嚴重。這不僅會大大增加 CPU 的使用率和處理器浪費的能源,而且還會給應用程式的性能帶來負面影響,這是因為占用鎖定的線程無法擷取處理器以釋放鎖定。在設計自旋等待循環時,使每個線程具有回退鎖定的功能是很重要的。這確定了不會有太多的線程同時主動擷取同一個鎖定。

回退某個鎖定可通過不同的方式實作。最簡單的方法是擷取計數鎖定,使用該方法,應用程式可以在嘗試擷取某一鎖定達到特定次數後,通過請求環境切換回退該鎖定。該嘗試計數易于調整,此方法也确實常常根據硬體線程數的不同而有所調整。自旋的挂鐘時間也将随處理器頻率發生變化,這使得此代碼難于維護。鎖定的指數回退首先将不斷嘗試擷取鎖定,但會将較長時間的等待視作線程不大可能獲得此鎖定。指數回退自旋等待具有更強的可伸縮性。在 Windows 網絡 [1] 中通常使用第二種方法。

圖 7 顯示了某個内聯程式集 xchg 鎖定在其自旋等待循環中具有回退功能和不具有回退功能時的效果。請注意具有回退功能的鎖定的性能遠遠高于不具有回退功能的鎖定的性能。這通常是因偶爾出現以下情況所緻:某個占用鎖定的線程根據環境被切換出去。該線程必須等待,直至擷取處理器後才能釋放鎖定。

WaitForSingleObject 與 EnterCriticalSection 性能比較

圖 7: ThreadID 鎖定核心上具有回退功能的鎖定和不具有回退功能的鎖定。針對可變讀取的自旋和針對鎖定嘗試的自旋開發人員在開發其自己的自旋等待循環時常犯的一個錯誤是試圖針對原子操作指令(而非可變讀取)進行自旋。針對髒讀進行自旋(而不是嘗試擷取鎖定)會消耗較少的時間和資源。這使得應用程式僅在鎖定可用時才嘗試擷取鎖定。

圖 8 顯示了對于 xchg 而言,針對鎖定自旋和針對可變讀取自旋所用時間的差異。

WaitForSingleObject 與 EnterCriticalSection 性能比較

圖 8: threadID 鎖定核心上具有回退功能的鎖定和不具有回退功能的鎖定。

一個常見的誤區是利用 cmpxchg 指令的鎖定的開銷低于利用 xchg 指令的鎖定的開銷。由于首先運作 cmp,cmpxchg 不會嘗試以獨占模式擷取鎖定,是以造成了此誤解。圖 9 顯示了 cmpxchg 指令與 xchg 指定的開銷并無太大差别。

WaitForSingleObject 與 EnterCriticalSection 性能比較

圖 9: 在 ThreadID 鎖定核心上比較 xchg 和 cmpxchg。結論針對各種不同的情況利用适當的鎖定對于確定 Microsoft Windows 環境中的性能和可伸縮性至關重要。即使不存在鎖定争用的情況下,利用 WaitForSingleObject 對于 Windows 核心而言,仍是開銷高昂的調用。如果将會頻繁調用該鎖定,并且隻存在少量針對該鎖定的争用,則應避免此類型的鎖定。是以,對于粒度鎖定,WaitForSingleObject 可能很危險。EnterCriticalSection 會首先嘗試擷取使用者級鎖定,如果存在鎖定争用,則會跳轉至核心。為幫助開發人員省去編寫自己的自旋等待循環的麻煩,Microsoft 添加了 TryEnterCriticalSection 調用,該調用在跳轉至核心之前會嘗試擷取某個自旋中的使用者級鎖定。

為了了解使用者級自旋等待循環,我們在此白皮書中提供了基本的構造和性能建議。回退功能和針對可變讀取的自旋對于自旋等待性能至關重要。參考資訊[1] http://www.microsoft.com/technet/itsolutions/network/deploy/depovg/tcpip2k.mspx*

[2] Chynoweth Michael針對英特爾® EM64T 或 32 位英特爾® 架構實作可伸縮的原子鎖定。from http://freeeim01.blog.163.com/blog/static/162339664201031944135253/