免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
Unity開發(fā)者的C#內(nèi)存管理之三

本文翻譯自:C# Memory Management for Unity Developers (part 1 of 3)

很多游戲時常崩潰,大多數(shù)情況下都是內(nèi)存泄露導致的。這系列文章詳細講解了內(nèi)存泄露的原因,如何找到泄露,又如何規(guī)避。

我要在開始這個帖子之前懺悔一下。雖然一直作為一個C / C++開發(fā)者,但是很長一段時間我都是微軟的C#語言和.NET框架的秘密粉絲。大約三年前,當我決定離開狂野的基于C / C++的圖形庫,進入現(xiàn)代游戲引擎的文明世界,Unity 帶著一個讓我毫不猶豫選擇它的特性脫穎而出。Unity 并不需要你用一種語言(如Lua或UnrealScript)‘寫腳本’卻用另外一種語言’編程’。相反,它對Mono有深度的支持,這意味著所有的編程可以使用任何.NET語言。哦,真開心!我終于有一個正當?shù)睦碛珊虲 ++說再見,而且通過自動內(nèi)存管理我所有的問題都得到了解決。此功能已經(jīng)內(nèi)置在C#語言,是其哲學的一個組成部分。沒有更多的內(nèi)存泄漏,沒有更多的考慮內(nèi)存管理!我的生活會變得容易得多。

如果你有哪怕是最基本的使用Unity或游戲編程的經(jīng)驗,你就知道我是多么的錯誤了。我費勁艱辛才了解到在游戲開發(fā)中,你不能依賴于自動內(nèi)存管理。如果你的游戲或中間件足夠復雜并且對資源要求很高,用C#做Unity開發(fā)就有點像往C ++方向倒退了。每一個新的Unity開發(fā)者很快學會了內(nèi)存管理是很麻煩的,不能簡單地托付給公共語言運行庫(CLR)。Unity論壇和許多Unity相關(guān)的博客包含一些內(nèi)存方面的技巧集合和最佳實不規(guī)范踐。不幸的是,并非所有這些都是基于堅實的事實,盡我所知,沒有一個是全面的。此外,在Stackoverflow這樣的網(wǎng)站上的C#專家似乎經(jīng)常對Unity開發(fā)者面對的古怪的、非標準的問題沒有一點耐心。由于這些原因,在這一篇和下面的兩篇帖子,我試著給出關(guān)于Unity特有的C#的內(nèi)存管理問題的概述,并希望能介紹一些深入的知識。

第一篇文章討論了在.NET和Mono的垃圾收集世界中的內(nèi)存管理基礎(chǔ)知識。我也討論了內(nèi)存泄漏的一些常見的來源。
第二篇著眼于發(fā)現(xiàn)內(nèi)存泄漏的工具。Unity的Profiler是一個強大的工具,但它也是昂貴的(似乎在中國不是)。因此,我將討論.NET反匯編和公共中間語言(CIL),以顯示你如何只用免費的工具發(fā)現(xiàn)內(nèi)存泄漏。
第三篇討論C#對象池。再次申明,重點只針對出現(xiàn)在Unity/ C#開發(fā)中的具體需要。

垃圾收集的限制
大多數(shù)現(xiàn)代操作系統(tǒng)劃分動態(tài)內(nèi)存為棧和堆(12),許多CPU架構(gòu)(包括你的PC / Mac和智能手機/平板電腦)在他們的指令集支持這個區(qū)分。 C#通過區(qū)分值類型支持它(簡單的內(nèi)置類型以及被聲明為枚舉或結(jié)構(gòu)的用戶自定義類型)和引用類型(類,接口和委托)。值類型在堆中,引用類型分配在棧上。堆具有固定大小,在一個新的線程開始時被設(shè)定。它通常很小 - 例如,NET線程在Windows默認為一個1MB的堆棧大小。這段內(nèi)存是用來加載線程的主函數(shù)和局部變量,并且隨后加載和卸載被主函數(shù)調(diào)用的函數(shù)(與他們的本地變量)。一些內(nèi)存可能會被映射到CPU的緩存,以加快速度。只要調(diào)用深度不過高或局部變量不過大,你不必擔心堆棧溢出。這種棧的用法很好地契合結(jié)構(gòu)化編程的概念(structured programming)。

如果對象太大不適合放在棧上,或者如果他們要比創(chuàng)造了他們的函數(shù)活得長,堆這個時候就該出場了。堆是“其他的一切“- 是一段可以隨著每個OS請求增長的內(nèi)存,and over which the program rules as it wishes(這句不會……)。不過,雖然棧幾乎是不能管理(只使用一個指針記住free section開始的地方),堆碎片很快會從分配對象的順序到你釋放的順序打亂。把堆想成瑞士奶酪,你必須記住所有的孔!根本沒有樂趣可言。進入自動內(nèi)存管理。自動分配的任務 - 主要是為你跟蹤奶酪上所有的孔 - 是容易的,而且?guī)缀醣凰械默F(xiàn)代編程語言支持。更難的是自動釋放,尤其是決定釋放的時機,這樣你就不必去管了。

后者任務被稱為垃圾收集(GC)。不是你告訴你的運行時環(huán)境什么時候可以釋放對象的內(nèi)存,是運行時跟蹤所有的對象引用,從而能夠確定——在特定的時間間隔里,一個對象不可能被你的代碼引用到了。這樣一個對象就可以被銷毀,它的內(nèi)存會被釋放。GC仍被學者積極地研究著,這也解釋了為什么GC的架構(gòu)自.net框架1.0版以來改變?nèi)绱酥?。然而,Unity不使用.net而是其開源的表親,Mono,而它一直落后于它的商業(yè)化對手(.net)。此外,Unity不默認使用Mono的最新版本(2.11/3.0),而是使用版本2.6(準確地說,2.6.5,在我的Windows4.2.2安裝版上(編輯:這同樣適用于Unity4.3])。如果你不確定如何自己驗證這一點,我將在接下來的帖子里討論。

在Mono2.6版本之后引入了有關(guān)GC的重大修改。新版本使用分代垃圾收集(generational GC),而2.6仍采用不太復雜的貝姆垃圾收集器(Boehm garbage collector)?,F(xiàn)代分代GC執(zhí)行得非常好,甚至可以在實時應用中使用(在一定限度內(nèi)),如游戲。另一方面,勃姆式GC的工作原理是在堆上做窮舉搜索垃圾。以一種相對“罕見”的時間間隔(即,通常的頻率大大低于一次每幀)。因此,它極有可能以一定的時間間隔造成幀率下降,因而干擾玩家。Unity的文檔建議您調(diào)用System.GC.Collect(),只要您的游戲進入幀率不那么重要的階段(例如,加載一個新的場景,或顯示菜單)。然而,對于許多類型的游戲,出現(xiàn)這樣的機會也極少,這意味著,在GC可能會在你不想要它的時候闖進來。如果是這樣的話,你唯一的選擇是自己硬著頭皮管理內(nèi)存。而這正是在這個帖子的其余部分,也是以下兩個帖子的內(nèi)容!

自己做內(nèi)存管理者

讓我們申明在Unity/.NET的世界里“自己管理內(nèi)存”意味著什么。你來影響內(nèi)存是如何分配的的力量是(幸運的)非常有限的。你可以選擇自定義的數(shù)據(jù)結(jié)構(gòu)是類(總是在堆上分配的)或結(jié)構(gòu)(在棧中分配,除非它們被包含在一個類中),并且僅此而已。如果你想要更多的神通,必須使用C#的不安全關(guān)鍵字。但是,不安全的代碼只是無法驗證的代碼,這意味著它不會在Unity Web Player中運行,還可能包括一些其他平臺。由于這個問題和其他原因,不要使用不安全的關(guān)鍵字。因為堆棧的上述限制,還因為C#數(shù)組是只是System.Array(這是一個類)的語法糖,你不能也不應該回避自動堆分配。你應該避免的是不必要的堆分配,我們會在這個帖子下一個(也是最后一個)部分講到這個。

當談到釋放的時候你的力量是一樣的有限。其實,可以釋放堆對象的唯一過程是GC,而它的工作原理是不可見的。你可以影響的是對任何一個對象的最后一個引用在堆中超出范圍的時機,因為在此之前,GC都不能碰他們。這種限制有巨大的實際意義,因為周期性的垃圾收集(你無法抑制)往往在沒有什么釋放的時候是非??斓?。這一事實為構(gòu)建對象池的各種方法提供了基礎(chǔ),我在第三篇帖子討論。

不必要的堆分配的常見原因

你應該避免foreach循環(huán)嗎?

在Unity 論壇和其他一些地方我經(jīng)常碰到的常見建議是避免foreach循環(huán),并用for或者while代替。乍一看理由似乎很充分。Foreach真的只是語法糖,因為編譯器會這樣把代碼做預處理:

foreach (SomeType s in someList)   s.DoSomething();...into something like the the following:using (SomeType.Enumerator enumerator = this.someList.GetEnumerator()){    while (enumerator.MoveNext())    {              SomeType s = (SomeType)enumerator.Current;       s.DoSomething();    }}

換句話說,每次使用foreach都會在后臺創(chuàng)建一個enumerator對象-一個System.Collections.IEnumerator接口的實例。但是是創(chuàng)建在堆上的還是在堆棧上的?這是一個好問題,因為兩種都有可能!最重要的是,在System.Collections.Generic 命名空間里幾乎所有的集合類型(List<T>, Dictionary<K, V>, LinkedList<T>, 等等)都會根據(jù)GetEnumerator()的實現(xiàn)聰明地返回一個struct。這包括伴隨著Mono2.6.5的所有集合版本。(Unity所使用)

Matthew Hanlon指出微軟現(xiàn)在的C#編譯器和Unity正在使用編譯你的腳本的老的Mono/c#編譯器之間一個不幸的差異。你也許知道你可以使用Microsoft Visual Studio來開發(fā)甚至編譯 Unity/Mono 兼容的代碼。你只需要將相應的程序集放到‘Assets’目錄下。所有代碼就會在Unity/Mono運行時環(huán)境中執(zhí)行。但是,執(zhí)行結(jié)果還是會根據(jù)誰編譯了代碼不一樣。Foreach循環(huán)就是這樣一個例子,這是我才發(fā)現(xiàn)的。盡管兩個編譯器都會識別一個集合的GetEnumerator()返回struct還是class,但是Mono/C#有一個會把struct-enumerator裝箱從而創(chuàng)建一個引用類型的BUG。

所以你覺得你該避免使用foreach循環(huán)嗎?

  • 不要在Unity替你編譯的時候使用
  • 在用最新的編譯器的時候可以使用用來遍歷standard generic collections (List<T> etc.)Visual Studio或者免費的 .NET Framework SDK 都可以,而且我猜測最新版的Mono 和 MonoDevelop也可以。

當你在用外部編譯器的時候用foreach循環(huán)來遍歷其他類型的集合會怎么樣?很不幸,沒有統(tǒng)一的答案。用在第二篇帖子里提到的技術(shù)自己去發(fā)現(xiàn)哪些集合是可以安全使用foreach的。

你應該避免閉包和LINQ嗎?

你可能知道C#提供匿名函數(shù)和lambda表達式(這兩個幾乎差不多但是不太一樣)。你能分別用delegate 關(guān)鍵字和=>操作符創(chuàng)建他們。他們通常都是很有用的工具,并且你在使用特定的庫函數(shù)的時候很難避免(例如List<T>.Sort()) 或者LINQ。

匿名方法和lambda會造成內(nèi)存泄露嗎?答案是:看情況。C#編譯器實際上有兩種完全不一樣的方法來處理他們。來看下面小段代碼來理解他們的差異:

1 int result = 0;   2 void Update(){   3 for (int i = 0; i < 100; i++)    {        4     System.Func<int, int> myFunc = (p) => p * p;       5      result += myFunc(i);    6 }}

正如你所看到的,這段代碼似乎每幀創(chuàng)建了myFunc委托 100次,每次都會用它執(zhí)行一個計算。但是Mono僅僅在Update()函數(shù)第一次調(diào)用的時候分配內(nèi)存(我的系統(tǒng)上是52字節(jié)),并且在后續(xù)的幀里不會再做任何堆的分配。怎么回事?使用代碼反射器(我會在下一篇帖子里解釋)就會發(fā)現(xiàn)C#編譯器只是簡單的把myFunc替換為System.Func<intint>類的一個靜態(tài)域。

我們來對這個委托的定義做一點點改變:

  System.Func<int, int> myFunc = (p) => p * i++;

通過把‘p’替換成’i++’,我們把可以稱為’本地定義的函數(shù)’變成了一個真正的閉包。閉包是函數(shù)式編程的核心。它們把函數(shù)和數(shù)據(jù)綁定在一起-更準確的說,是和在函數(shù)外定義的非本地變量綁定。在myFunc這個例子里,’p’是一個本地變量但是’i’不是,它屬于Update()函數(shù)的作用域。C#編譯器現(xiàn)在得把myFunc轉(zhuǎn)換成可以訪問甚至改變非本地變量的函數(shù)。它通過聲明(后臺)一個新類來代表myFunc創(chuàng)造時的引用環(huán)境來達到這個目的。這個類的對象會在我們每次經(jīng)歷for循環(huán)的時候創(chuàng)建,這樣我們就突然有了一個巨大的內(nèi)存泄露(在我的電腦上2.6kb每幀)。

當然,在C#3.0引入閉包和其他一些語言特性的主要原因是LINQ。如果閉包會導致內(nèi)存泄露,那在游戲里使用LINQ是安全的嗎?也許我不適合問這個問題,因為我總是像躲瘟疫一樣避免使用LINQ。LINQ的一部分顯然不會在不支持實時編譯(jit)的系統(tǒng)上工作,比如iOS。但是從內(nèi)存角度考慮,LINQ也不是好的選擇。一個像這樣基礎(chǔ)到難以置信的表達式:

 

1 int[] array = { 1, 2, 3, 6, 7, 8 };2 void Update(){   3  IEnumerable<int> elements = from element in array                    4 orderby element descending                   5  where element > 2                    6 select element;    ...}

在我的系統(tǒng)上每幀需分配68字節(jié)(Enumerable.OrderByDescending()分配28,Enumerable.Where()40)!這里的元兇甚至不是閉包而是IEnumerable的擴展方法:LINQ必須得創(chuàng)建中間數(shù)組以得到最終結(jié)果,并且之后沒有適當?shù)南到y(tǒng)來回收。雖然這么說,但我也不是LINQ方面的專家,我也不知道是否部分可以再實際中可以使用。

協(xié)程

如果你通過StartCoroutine()來啟動一個協(xié)程,你就隱式創(chuàng)建了一個UnityCoroutine類(21字節(jié))和一個Enumerator 類(16字節(jié))的實例。重要的是,當協(xié)程 yield和resume的時候不會再分配內(nèi)存,所以你只需要在游戲運行的時候限制StartCoroutine() 的調(diào)用就能避免內(nèi)存泄露。

字符串

對C#和Unity內(nèi)存問題的概論不提及字符串是不完整的。從內(nèi)存角度考慮,字符串是奇怪的,因為它們既是堆分配的又是不可變的。當你這樣連接兩個字符串的時候:

1 void Update(){   2  string string1 = "Two";   3  string string2 = "One" + string1 + "Three";4 }

運行時必須至少分配一個新的string類型來裝結(jié)果。在String.Concat()里這會通過一個叫FastAllocateString()的外部函數(shù)高效的執(zhí)行,但是沒有辦法繞過堆分配(在我的系統(tǒng)里上述例子占用40字節(jié))。如果你需要動態(tài)改變或者連接字符串,使用System.Text.StringBuilder。

裝箱

有時候,數(shù)據(jù)必須在堆棧和堆之間移動。例如當你格式化這樣的一個字符串:

string result = string.Format("{0} = {1}", 5, 5.0f);

你是在調(diào)用這樣的函數(shù):

 

1 public static string Format(    2 string format,    3 params Object[] args)

換句話說,當調(diào)用Format()的時候整數(shù)5和浮點數(shù)’5.0f’必須被轉(zhuǎn)換成System.Object。但是Object是一個引用類型而另外兩個是值類型。C#因此必須在堆上分配內(nèi)存,將值拷貝到堆上去,然后處理Format()到新創(chuàng)建的int和float對象的引用。這個過程就叫裝箱,和它的逆過程拆箱。

對 String.Format()來說這個行為也許不是一個問題,因為你怎樣都希望它分配堆內(nèi)存(為新的字符串)。但是裝箱也會在意想不到的地方發(fā)生。最著名的一個例子是發(fā)生在當你想要為你自己的值類型實現(xiàn)等于操作符“==”的時候(例如,代表復數(shù)的結(jié)構(gòu))。閱讀關(guān)于如果避免隱式裝箱的例子點這里here

庫函數(shù)

為了結(jié)束這篇帖子,我想說許多庫函數(shù)也包含隱式內(nèi)存分配。發(fā)現(xiàn)它們最好的方法就是通過分析。最近遇到的兩個有趣的例子是:

  • 之前我提到foreach循環(huán)通過大部分的標準泛集合類型并不會導致堆分配。這對Dictionary<K, V>也成立。然而,神奇的是,Dictionary<K, V>集合和Dictionary<K, V>.Value集合是類類型,而不是結(jié)構(gòu)。意味著 “(K key in myDict.Keys)…”需要占用16字節(jié)。真惡心!
  • List<T>.Reverse()使用標準的原地數(shù)組翻轉(zhuǎn)算法。如果你像我一樣,你會認為這意味著不會分配堆內(nèi)存。又錯了,至少在Mono2.6里。有一個擴展方法你能使用,但是不像.NET/Mono版本那樣優(yōu)化過,但是避免了堆分配。和使用List<T>.Reverse()一樣使用它:
public static class ListExtensions{    public static void Reverse_NoHeapAlloc<T>(this List<T> list)    {            int count = list.Count;            for (int i = 0; i < count / 2; i++)        {               T tmp = list[i];                  list[i] = list[count - i - 1];                 list[count - i - 1] = tmp;        }    }}                    

還有其他的內(nèi)存陷阱可以寫的。但是,我不想給你更多的魚了,而是教你自己捕魚。這就是下篇帖子的內(nèi)容!

本站僅提供存儲服務,所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
Unity3D里foreach,using和Coroutine的GC問題探究及解決方案 | Atlantis技術(shù)博客
漫談 Unity 游戲開發(fā)中 C# 的精髓
Unity3D游戲GC優(yōu)化總結(jié)
【Unity面試篇】Unity 面試題總結(jié)甄選 |C#基礎(chǔ)篇 | ??持續(xù)更新??
騰訊大咖說:騰訊是如何做Unity手游性能優(yōu)化的
性能優(yōu)化,進無止境---內(nèi)存篇(上)
更多類似文章 >>
生活服務
分享 收藏 導長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服