首頁  >  文章  >  後端開發  >  編寫高效能 .NET的實例教學課程

編寫高效能 .NET的實例教學課程

零下一度
零下一度原創
2017-06-25 09:08:431524瀏覽

減少分配率

這個幾乎不用解釋,減少了記憶體的使用量,自然就減少GC回收時的壓力,同時降低了記憶體碎片與CPU的使用量。你可以用一些方法來達到這個目的,但它可能會與其它設計相衝突。

你需要在設計物件時仔細檢查每個它並問自己:

  1. #我真的需要這個物件嗎?

  2. 這個欄位是我需要的嗎?

  3. 我能減少陣列的尺寸嗎?

  4. 我能縮小primitives的尺寸嗎(用Int32替換Int64,其它)?

  5. 這些對象,是否只有在極少數情況下,或只有初始化的時候才會被使用?

  6. 是否能將一些類別轉為結構體使他們在堆疊上分配或成為某個物件的一部分?

  7. 我是否分配了大量內存,但實際上只使用其中很小的一部分?

  8. 我可以從其它地方拿到相關資料?

小故事:在服務端一個回應請求的函數裡,我們發現在一次請求裡會分配一些比記憶體段要大的記憶體。這樣導致每次請求我們都會觸發一次完整的GC,這是因為CLR要求所有的0代物件都在一個記憶體段裡,目前分配的記憶體段滿了,就會開闢一個新的記憶體段,同時對原先的記憶體段做一次2代的回收。這不是一個好的實現,因為我們除了減少記憶體分配外別無它法。

最重要的規則

對於垃圾回收的高效能程式設計有一個基本規則,事實上也是程式碼設計的指導規則。

要收集的物件要麼在0代,要麼不存在
Collect objects in gen 0 or not at all.

#不同的是,你希望一個物件擁有極短的生命週期,在GC的時候永遠不要碰到它,或者,如果你做不到這一點,它們應該去2代,盡可能的快,永遠的呆在那,永遠不會被回收。這意味著你永遠保持對長生命週期物件的引用。通常,也意味著物件可重複使用,尤其是在大物件堆裡的物件。
GC每高一個世代的回收會比上一個世代更耗時。如果你想保持許多0,1代和少量的2代物件。即使開啟後台GC做2代做回收,也會消耗相當CPU運算量,你可能更願意將這部分的CPU消耗給應用程序,而不是GC。

Note 你可能聽過一個說法,每10次0代的回收會產生一次1代的回收,每10次1代的回收會產生1次2代的回收。這其實是不正確的,但你要明白,你要盡量產生多次快速的0代回收,以及少量的2代回收。

你最好避免進行1代回收,主要是因為已經從0代提升到1代的對象,會在這時候被轉入2代。 1代是物件進入2代的一個緩衝區。
理想情況下,你分配的每一個物件應該在下一次0代回收前結束生命週期。你可以測量兩次GC的時間間隔,並將其與應用程式裡物件的生命週期長度做對比。有關如何使用工具測量生命週期的信息,可以在本章結尾看到。
你可能不習慣這樣思考,但這規則切入了應用程式的方方面面,你需要經常思考它,在心態要做根本的轉變,這樣才能實現這個最重要的規則。

縮短物件的生命週期

一個物件的作用範圍越短,在下一個GC出現時,它被提升到下一代的機會就越小。一般來說,在你需要之前,不要創建物件。
同時,當物件創建的代價如此之高時,異常就可以在較早的時候創建,這樣不會幹擾到其他處理邏輯。
另外,你也要確保物件盡可能早的退出作用域。對於局部變量,你可以在最後一次使用後,甚至在方法結束前將其生命週期結束。你可一個用{}將程式碼包含起來,這不會對你的運行產生影響,但編譯器會認為在這個範圍的物件已經完成了他的生命週期,不再被使用了。如果需要呼叫物件的方法,盡量減少第一次和最後一次的時間間隔,以便GC儘早的回收物件。
如果物件關聯(引用)了一些會長時間保持的對象,則需要解除他們的引用關係。你可能會有更多的空值檢查(null判斷),這可能會讓程式碼變得更複雜。也會在物件的可用狀態(always having full state available)上與效率之間造成緊張關係,特別是調試的時候。
解決的一種方法是,將要清空的物件轉換為另一種方式存在,例如:日誌訊息,這樣在後面的偵錯時可以查詢到相關資訊。
另一個方法是為程式碼增加可設定選項(不解除物件之間的關係):執行程式(或執行程式裡特定的一個部分,例如一個特定的請求),在這個模式中沒有解除對象引用關係,而是盡可能讓物件一直保持方便調試。

減少物件層次的深度

如本章開頭所述,GC在回收時會順著物件的參考關係進行遍歷。在伺服器GC模式,GC會以多執行緒方式執行,但如果一個執行緒需要處理一個物件層次很深,則所有已經處理完的執行緒都需要等待這個執行緒完成處理後才能退出。在今後的CLR版本裡,你可以不用太關注這個問題,GC在多執行緒執行時會採用更好的標記演算法來做負載平衡。但如果你物件層次很深,這個問題還是要注意一下的。

減少物件之間的引用

這與上節的深度有關,但也有一些其它的因素。
一個物件如果引用了很多物件(數組,List吧),那它將花很多時間在遍歷物件上。是GC造成長時間的問題,因為它有一個複雜的關係圖。
另一個問題是,如果無法輕鬆的確定物件有多少引用關係,那麼你就無法準確的預測物件的生命週期。減少這種複雜度是相當有必要的,它不但可以讓程式碼更健壯,同時也方便調試以及獲得更好的效能。
另外,也要注意不同世代物件之間的引用也會導致GC的效率低下,特別是舊物件對新物件的引用。例如,如果2代對像在0代對象裡有引用關係,那麼每次發生0代的GC時,也需要掃描部分2代對象,看看他們是否仍保持在0代對象的引用上。雖然這不是一次完整的GC,但它仍然是不要的工作,你應該盡量避免這種情況。

避免釘住物件(Pinning)

釘住物件可以保證從託管程式碼傳送資料到本機程式碼的安全性。常見的有數組和字串。如果你的程式碼不需要與本地程式碼做交互,則不用考慮它的效能開銷。
釘住物件就是讓物件在垃圾回收(壓縮階段)時無法移動他。雖然釘住物件不會造成太多開銷,但它會妨礙到GC的回收操作,增加記憶體碎片的可能性。 GC在回收時會記錄物件的位置,以便在重修分配時利用它們之間的空間,但如果釘住的物件很多,會導致記憶體碎片的增加。
釘可以是顯示的也可以使隱式的。顯示的是使用GCHandle時用GCHandleType.Pinned參數進行設置,或在unsafe模式下使用 fixed 關鍵字。使用fixed關鍵字和GCHandle的差異在於是否會顯示呼叫Dispose方法。使用fixed雖然很方便,但是不能在非同步情況下使用,但還是可以建立一個句柄物件(GCHandle),在回呼時傳回並處理。
隱式的釘住物件比較常見,但也更難排查,也更難移除。最明顯的例子就是透過平台呼叫(P/Invoke)將物件傳遞給非託管程式碼。這不只是你的程式碼—--你經常呼叫的一些託管API,其實也是會呼叫本機程式碼,也會將物件釘住。
CLR也會將自己的一些資料釘住,但這通常不需要你來關心。
理想情況下,你應該盡可能的不要釘住對象。如果不能做到,那麼遵循先前的重要規則,盡可能讓這些被釘的對象儘早釋放。如果物件只是簡單的被釘住後釋放,那麼就不會有多少機會影響回收操作。你同時也要避免同時釘住很多個物件。被釘的對像被交換到2代或在LOH裡分配會稍微好些。根據這個規則,你可以在大物件堆上分配一個大的緩衝區,並根據實際需要自己對緩衝區做管理。或在小物件對上分配緩衝區,然後在釘住他們之前,使他們升級到2代。這樣比你直接將對象釘在0代上要好。

以上是編寫高效能 .NET的實例教學課程的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn