您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關(guān)如何編寫高性能.NET,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
這個幾乎不用解釋,減少了內(nèi)存的使用量,自然就減少GC回收時的壓力,同時降低了內(nèi)存碎片與CPU的使用量。你可以用一些方法來達到這一目的,但它可能會與其它設(shè)計相沖突。
你需要在設(shè)計對象時仔細檢查每個它并問自己:
我真的需要這個對象嗎?
這個字段是我需要的嗎?
我能減少數(shù)組的尺寸嗎?
我能縮小primitives的尺寸嗎(用Int32替換Int64,其它)?
這些對象,是否只有在極少數(shù)情況下,或者只有初始化的時候才用到?
是否能將一些類轉(zhuǎn)為結(jié)構(gòu)體使他們在棧上分配或者成為某個對象的一部分?
我是否分配了大量內(nèi)存,但實際只使用其中很小的一部分?
我可以從其它地方拿到相關(guān)數(shù)據(jù)?
小故事:在服務(wù)端一個響應(yīng)請求的函數(shù)里,我們發(fā)現(xiàn)在一次請求里會分配一些比內(nèi)存段要大的內(nèi)存。這樣導致每次請求我們都會觸發(fā)一次完整的GC,這是因為CLR要求所有的0代對象都在一個內(nèi)存段里,當前分配的內(nèi)存段滿了,就會開辟一個新的內(nèi)存段,同時對原先的內(nèi)存段做一次2代的回收。這不是一個好的實現(xiàn),因為我們除了減少內(nèi)存分配外別無它法。
對于垃圾回收的高性能編程有一個基本規(guī)則,事實上也是代碼設(shè)計的指導規(guī)則。
要收集的對象要么在0代,要么不存在
Collect objects in gen 0 or not at all.
不同的是,你希望一個對象擁有極短的生命周期,在GC的時候永遠不要碰到它,或者,如果你做不到這一點,它們應(yīng)該去2代,盡可能的快,永遠的呆在那,永遠不會被回收。這意味著你永遠保持對長生命周期對象的引用。通常,也意味著對象可重復使用,尤其是在大對象堆里的對象。
GC每高一個世代的回收會比上一個世代更加耗時。如果你想保持許多0,1代和少量的2代對象。即使開啟后臺GC做2代做回收,也會消耗相當CPU運算量,你可能更愿意將這部分的CPU消耗給應(yīng)用程序,而不是GC。
Note 你可能聽過一個說法,每10次0代的回收會產(chǎn)生一次1代的回收,每10次1代的回收會產(chǎn)生1次2代的回收。這其實是不正確的,但是你要明白,你要盡可能產(chǎn)生多次快速的0代回收,以及少量的2代回收。
你最好避免進行1代回收,主要是因為已經(jīng)從0代提升到1代的對象,會在這時候被轉(zhuǎn)入2代。1代是對象進入2代的一個緩沖區(qū)。
理想情況下,你分配的每一個對象應(yīng)該在下一次0代回收前結(jié)束生命周期。你可以測量兩次GC的時間間隔,并將其與應(yīng)用程序里對象的生命周期長度做對比。有關(guān)如何使用工具測量生命周期的信息,可以在本章結(jié)尾看到。
你可能不習慣這樣思考,但這規(guī)則切入了應(yīng)用程序的方方面面,你需要經(jīng)常思考它,在心態(tài)要做根本的轉(zhuǎn)變,這樣才能實現(xiàn)這個最重要的規(guī)則。
一個對象的作用范圍越短,在下一個GC出現(xiàn)時,它被提升到下一代的機會就越小。一般來說,在你需要之前,不要創(chuàng)建對象。
同時,當對象創(chuàng)建的代價如此之高時,異常就可以在較早的時候創(chuàng)建,這樣不會干擾到其他處理邏輯。
另外,你還要確保對象盡可能早的退出作用域。對于局部變量,你可以在最后一次使用后,甚至在方法結(jié)束前將其生命周期結(jié)束。你可一個用{}將代碼包括起來,這不會對你的運行產(chǎn)生影響,但編譯器會認為在這個范圍的對象已經(jīng)完成了他的生命周期,不再被使用了。如果需要調(diào)用對象的方法,盡量減少第一次和最后一次的時間間隔,以便GC盡早的回收對象。
如果對象關(guān)聯(lián)(引用)了一些會長時間保持的對象,則需要解除他們的引用關(guān)系。你可能會有更多的空值檢查(null判斷),這可能會讓代碼變得更復雜。也會在對象的可用狀態(tài)(always having full state available)上與效率之間造成緊張關(guān)系,特別是調(diào)試的時候。
解決的一種方法是,將要清空的對象轉(zhuǎn)換為另外一種方式存在,例如:日志消息,這樣在后面的調(diào)試時可以查詢到相關(guān)信息。
另外一種方法是為代碼增加可配置選項(不解除對象之間的關(guān)系):運行程序(或者運行程序里特定的一個部分,例如一個特定的請求),在這個模式中沒有解除對象引用關(guān)系,而是盡可能讓對象一直保持方便調(diào)試。
如本章開頭所述,GC在回收時會順著對象的引用關(guān)系進行遍歷。在服務(wù)器GC模式,GC會以多線程方式運行,但如果一個線程需要處理一個對象層次很深,則所有已經(jīng)處理完的線程都需要等這個線程完成處理后才能退出。在今后的CLR版本里,你可以不用太關(guān)注這個問題,GC在多線程執(zhí)行時會采用更好的標記算法做負載均衡。但如果你對象層次很深,這個問題還是要關(guān)注一下的。
這與上節(jié)的深度有關(guān),但也有一些其它的因素。
一個對象如果引用了很多對象(數(shù)組,List吧),那它將花很多時間在遍歷對象上。是GC造成長時間的一個問題,因為它有一個復雜的關(guān)系圖。
另外一個問題是,如果無法輕松的確定對象有多少引用關(guān)系,那么你就無法準確的預測對象的生命周期。減少這種復雜度是相當有必要的,它不但可以讓代碼更健壯,同時也方便調(diào)試以及獲得更好的性能。
另外,還要注意不同世代對象之間的引用也會導致GC的效率低下,特別是舊對象對新對象的引用。例如,如果2代對象在0代對象里有引用關(guān)系,那么每次發(fā)生0代的GC時,也需要掃描部分2代對象,看看他們是否仍然保持到0代對象的引用上。雖然這不是一次完整的GC,但它仍然是不要的工作,你應(yīng)該盡量避免這種情況。
釘住對象可以保證從托管代碼往本地代碼里傳遞數(shù)據(jù)的安全。常見的有數(shù)組和字符串。如果你的代碼不需要與本地代碼做交互,則不用考慮它的性能開銷。
釘住對象就是讓對象在垃圾回收(壓縮階段)時無法移動他。雖然釘住對象不會造成多大開銷,但它會妨礙到GC的回收操作,增加內(nèi)存碎片的可能性。GC在回收時會記錄對象的位置,以便在重修分配時利用它們之間的空間,但如果釘住的對象很多,會導致內(nèi)存碎片的增加。
釘可以是顯示的也可以使隱式的。顯示的是使用GCHandle時用GCHandleType.Pinned參數(shù)進行設(shè)置,或者在unsafe模式下使用 fixed 關(guān)鍵字。使用fixed關(guān)鍵字和GCHandle的差別在于是否會顯示調(diào)用Dispose方法。使用fixed雖然很方便,但是不能在異步情況下使用,但還是可以創(chuàng)建一個句柄對象(GCHandle),在回調(diào)時傳回并處理。
隱式的釘住對象則比較常見,但也更難排查,也更難移除。最明顯的例子就是通過平臺調(diào)用(P/Invoke)將對象傳遞給非托管代碼。這不僅僅是你的代碼—--你經(jīng)常調(diào)用的一些托管API,實際上也是會調(diào)用本地代碼,也會將對象釘住。
CLR也會將自己的一些數(shù)據(jù)給釘住,但這通常不需要你來關(guān)心。
理想情況下,你應(yīng)該盡可能的不要釘住對象。如果不能做到,那么遵循之前的重要規(guī)則,盡可能讓這些被釘?shù)膶ο蟊M早釋放。如果對象只是簡單的被釘住后釋放,那么也不會有多少機會影響回收操作。你同時也要避免同時釘住很多個對象。被釘?shù)膶ο蟊唤粨Q到2代或者在LOH里分配會稍微好些。根據(jù)這個規(guī)則,你可以在大對象堆上分配一個大的緩沖區(qū),并根據(jù)實際需自己對緩沖區(qū)做管理?;蛘咴谛ο髮ι戏峙渚彌_區(qū),然后在釘住他們前,使他們升級到2代。這樣比你直接將對象釘在0代上要好。
關(guān)于“如何編寫高性能.NET”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。