栈到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,欢迎转载,若有不对的地方但愿指正。

咱们部门运营的公众号,里面有不少技术文章和最新热点,欢迎关注:

 

相关文章
相关标签/搜索