棧到CLR

提起棧想必會聽到這樣幾個關鍵詞:後進先出,先進後出,入棧,出棧。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;
        }
View Code

 入棧:咱們也看到這個是基於數組而且支持的那個太擴容的棧,默認大小圍爲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++;
        }
View Code

 

正如註釋中提到的,入棧複雜度能夠達到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     }
View Code

 

 咱們隱藏這些方法的具體實現,只預覽他們的之間的關係:

接下來,線程即將進入到下面的這個方法:

        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,歡迎轉載,若有不對的地方但願指正。

咱們部門運營的公衆號,裏面有不少技術文章和最新熱點,歡迎關注:

 

相關文章
相關標籤/搜索