提起棧想必會聽到這樣幾個關鍵詞:後進先出,先進後出,入棧,出棧。html
棧這種數據結構,數組徹底能夠代替其功能。算法
可是存在便是真理,其目的就是避免暴漏沒必要要的操做。編程
如角色同樣,不一樣的情景或者角色擁有不一樣的操做權限。c#
那咱們來了解一下棧,棧是一種線性數據結構,而且只能從一端壓入或者彈出 = 添加或者刪除。數組
基於數組實現的棧是順序棧,基於鏈表實現的鏈式棧。數據結構
接下來咱們看一下.Net 是怎麼實現棧的?ide
註釋說的很明白:這是一個基於數組實現的順序棧,其入棧複雜度O(n),出棧複雜度爲O(1)函數
// A simple stack of objects. Internally it is implemented as an array, // so Push can be O(n). Pop is O(1). [DebuggerTypeProxy(typeof(System.Collections.Stack.StackDebugView))] [DebuggerDisplay("Count = {Count}")] [System.Runtime.InteropServices.ComVisible(true)] [Serializable] public class Stack : ICollection, ICloneable { private Object[] _array; // Storage for stack elements [ContractPublicPropertyName("Count")] private int _size; // Number of items in the stack. private int _version; // Used to keep enumerator in sync w/ collection. [NonSerialized] private Object _syncRoot; private const int _defaultCapacity = 10; public Stack() { _array = new Object[_defaultCapacity]; _size = 0; _version = 0; }
入棧:咱們也看到這個是基於數組而且支持的那個太擴容的棧,默認大小圍爲10,當存滿以後就會兩倍擴容。性能
// Pushes an item to the top of the stack. // public virtual void Push(Object obj) { //Contract.Ensures(Count == Contract.OldValue(Count) + 1); if (_size == _array.Length) { Object[] newArray = new Object[2*_array.Length]; Array.Copy(_array, 0, newArray, 0, _size); _array = newArray; } _array[_size++] = obj; _version++; }
正如註釋中提到的,入棧複雜度能夠達到O(n),出棧能夠是O(1) spa
so Push can be O(n). Pop is O(1).
出棧Pop,複雜度爲O(1)很好理解,
上面提到的動態棧是操做受限的數組,而且不會產生新的內存申請或者數據的搬移
咱們只要是獲取到最後一個,而後彈出(也就是刪除)便可。
入棧Push,咱們接下來分析一下出棧爲何複雜度爲O(n):
前面有一篇《算法複雜度》 https://www.cnblogs.com/sunchong/p/9928293.html ,提到過:最好、最壞、平均
對於下面的入棧代碼:
最好狀況時間複雜度:不會有擴容和數據遷移,因此直接追加便可,複雜度爲O(1)
最壞狀況時間複雜度:須要擴容後而且搬移原來n個數據,而後再插入新的數據,複雜度爲 O(n)
那麼平均時間複雜度是多少呢?這裏須要結合攤還分析法,進行復雜度分析。爲何這裏要使用攤還分析法呢?
先耐心地看看其定義:
分析一個操做序列中所執行的全部操做的平均時間分析方法。
與通常的平均分析方法不一樣的是,它不涉及機率的分析,能夠保證最壞狀況下每一個操做的平均性能。
總結一下攤還分析:執行的全部操做的平均時間,不扯機率。
一頭霧水也沒關係,咱們能夠拿攤還分析來直接分析:
public virtual void Push(Object obj) { //Contract.Ensures(Count == Contract.OldValue(Count) + 1); if (_size == _array.Length)
{ Object[] newArray = new Object[2*_array.Length]; Array.Copy(_array, 0, newArray, 0, _size); _array = newArray; } _array[_size++] = obj; _version++; }
入棧的最壞狀況複雜度是O(n),可是這個最壞狀況時間複雜度是在n+1次插入的時候發生的,
剩下的n次是不須要擴容搬移數據,只是簡單的入棧 O(1),因此均攤下來的複雜度是O(1)。
那麼爲何微軟的工程師在備註裏寫下push複雜度是O(n)?--這裏指的是空間複雜度O(n)
棧這種數據結構的應用有不少場景,其中一種就是咱們的線程棧或者說函數棧。
當開啓一個線程時,Windows系統爲每一個線程分配一個1M大小的線程棧。分配這個用來幹什麼呢?
存儲方法中的參數和變量。趁這個機會,咱們瞭解一下CLR的內存模型。
首先對於C#代碼時如何編程機器代碼的呢?
c#代碼 -> 編譯器 -> IL -> JIT -> CPU指令
當一個線程執行到如下方法時會有什麼操做呢?
這段代碼也很簡單,就是在Main方法中調用了GetCode()方法,其餘的都是一些臨時變量。
static void Main(string[] args) { string parentCode = "PI001"; string newCode = string.Empty; newCode = GetCode(parentCode); } static string GetCode(string sourceCode) { string currentMaxCode = "001001"; string newCode = $"{sourceCode}{currentMaxCode}"; return newCode; }
首先線程會分配1M內存用於存儲臨時變量和參數;
進入Main方法時,會將參數和返回地址依次壓棧
static void Main(string[] args)
string parentCode = "PI001"; string newCode = string.Empty;
接下來開始進入到 GetCode() 方法,此時與以前同樣壓入參數和返回地址
string currentMaxCode = "001001"; string newCode = $"{sourceCode}{currentMaxCode}";
既然說到了棧,那咱們不得再也不說一下託管堆,再來一段代碼圖解:
這是父類和子類的具體代碼,能夠略過此處。
1 public class BaseProduct 2 { 3 public string GetCode() 4 { 5 string maxCode = "001"; 6 return maxCode; 7 } 8 9 public virtual string Trim(string source) 10 { 11 return source.Trim(); 12 } 13 } 14 15 public class Product : BaseProduct 16 { 17 private static string _type="PI"; 18 19 public override string Trim(string source) 20 { 21 return source.Replace(" ", string.Empty); 22 } 23 24 public static string GetNewCode(string parentCode) 25 { 26 string currentMaxCode = "001001001"; 27 string newCode = $"{parentCode}{currentMaxCode}"; 28 return newCode; 29 30 } 31 }
咱們隱藏這些方法的具體實現,只預覽他們的之間的關係:
接下來,線程即將進入到下面的這個方法:
void GetProduct() { BaseProduct p; string sourceCode; p = new Product(); sourceCode = p.GetCode(); sourceCode = Product.GetNewCode(sourceCode); }
JIT在編譯到此方法前會注意到這個方法全部的引用類型,
並向CLR發出通知,加載程序集,在託管對中建立相關的類型對象數據結構。
這也就是咱們所能理解的靜態字段爲何不是在實例化的時候建立,
而是在這個類型建立的時候就一直存在。
這其實就是兩個概念,靜態資源是屬於類結構的,而實例資源時屬於實例自己。
下面的圖忽略String類型,由於String類型是經常使用類型可能在以前就已經建立好了,
類型對象包括:對象指針、同步塊索引、靜態資源、方法表,像下面這樣:
BaseProduct p; string sourceCode;
方法變量入棧,而且引用類型初始化爲null
p = new Product();
實例化Product ,託管堆建立Product對象,並將這個對象的指針指向Product類型。
將線程棧中的變量p指針,指向新建立的Product對象。
sourceCode = p.GetCode();
JIT找到這個變量p的Product對象,再找到對應的Product類型,表中找到此方法GetCode();
固然這個方法實際是父類的方法,因此JIT會一直向上找,直到找到爲止。
圖中是個虛擬路線,計算結果賦值給 string sourceCode
sourceCode = Product.GetNewCode(sourceCode);
和上一步相似,只不過此次是調用的靜態方法。發出類型Product,靜態方法是GetNewCode()
以上內容就是 JIT、CLR、線程棧、託管堆的一些運行時關係。
本文連接:https://www.cnblogs.com/sunchong/p/10011657.html,歡迎轉載,若有不對的地方但願指正。
咱們部門運營的公衆號,裏面有不少技術文章和最新熱點,歡迎關注: