SQL Server 数据库的一些基本概念

1.区:

区是SQL Server 中管理空间的基本单位。

一个区是八个物理上连续的页(即 64 KB), 所有页都存储在区中, 这意味着 SQL Server 数据库中每 MB 有 16 个区。 一旦一个区段已满, 下一条数据Sql server将分配一个区段空间, 防止每次添加都要分配空间.

2.页(Page):

页是SQL Server 中数据存储的基本单位, 它是区段的分配单元, 一页8K, 它下面就是数据行了, 但每页的行数不定, 这取决于数据行的大小.

数据库中的数据文件(.mdf 或 .ndf)分配的磁盘空间可以从逻辑上划分成页(从 0 到 n 连续编号)。磁盘 I/O 操作在页级执行。也就是说,SQL Server 每次读取或写入数据的最少数据单位是数据页。

每页的开头是 96 字节的标头,用于存储有关页的系统信息。此信息包括页码、页类型、页的可用空间以及拥有该页的对象的分配单元 ID。

在 SQL Server 中,页的大小为 8 KB。这意味着 SQL Server 数据库中每 MB 有 128 页。依次类推。根据数据库的文件大小,我们可以算出数据库有多少数据页。

3.页的类型: 数据页, 索引页, Blob页等等.

4.行: 因为行存于页中,因此, 一行的大小通常最多8K(8060字符, 一页的上限), 一行最大列数为1024列(字段).
如果是varchar(max), text, image时, 可以跨越多页, 一行最大2GB, 此时, 原始的行用来存放指针及其它列.

5.全文目录: 虽然和sql server在一起, 但实际上该目录是独立存放在磁盘上的.

6.索引: 是与表或视图关联的磁盘上的结构,可以加快从表或视图中检索行的速度。

它包含由表或视图中的一列或多列生成的键。这些键存储在一个结构(B 树)中,使 SQL Server 可以快速有效地查找与键值关联的行。

7.索引的类型:聚集, 非聚集.

另外还有唯一索引, 唯一索引确保索引键不包含重复的值,因此,表或视图中的每一行在某种程度上是唯一的。

聚集索引和非聚集索引都可以是唯一索引。

8.聚集索引: 根据数据行的键值在表或视图中排序和存储这些数据行。

每个表只能有一个聚集索引,因为数据行本身只能按一个顺序排序。

只有当表包含聚集索引时,表中的数据行才按排序顺序存储。如果表具有聚集索引,则该表称为聚集表。

如果表没有聚集索引,则其数据行存储在一个称为堆的无序结构中。

即聚集表是有聚集索引的表,堆是没有聚集索引的表。

索引视图与聚集表具有相同的存储结构。

聚集索引的叶节点就是实际的数据页。

聚集索引的平均大小大约为表大小的5%左右。

9.非聚集索引:

每个表最多可以有249个非聚集索引.

10.索引的维护:每当修改了表数据后,都会自动维护表或视图的索引。

11.索引和约束: 对表列定义了 PRIMARY KEY 约束和 UNIQUE 约束时,会自动创建索引。

下面摘自MSDN:

页和区

SQL Server 中数据存储的基本单位是页。为数据库中的数据文件(.mdf 或 .ndf)分配的磁盘空间可以从逻辑上划分成页(从 0 到 n 连续编号)。磁盘 I/O 操作在页级执行。也就是说,SQL Server 读取或写入所有数据页。

区是八个物理上连续的页的集合,用来有效地管理页。所有页都存储在区中。

在 SQL Server 中,页的大小为 8 KB。这意味着 SQL Server 数据库中每 MB 有 128 页。每页的开头是 96 字节的标头,用于存储有关页的系统信息。此信息包括页码、页类型、页的可用空间以及拥有该页的对象的分配单元 ID。

下表说明了 SQL Server 数据库的数据文件中所使用的页类型。

页类型 内容
Data text in row 设置为 ON 时,包含除 text ntextimagenvarchar(max)varchar(max)varbinary(max)xml 数据之外的所有数据的数据行。
Index 索引条目。
Text/Image 大型对象数据类型:

  • text ntextimagenvarchar(max)varchar(max)varbinary(max)xml 数据。

数据行超过 8 KB 时为可变长度数据类型列:

  • varcharnvarcharvarbinarysql_variant
Global Allocation Map、Shared Global Allocation Map 有关区是否分配的信息。
Page Free Space 有关页分配和页的可用空间的信息。
Index Allocation Map 有关每个分配单元中表或索引所使用的区的信息。
Bulk Changed Map 有关每个分配单元中自最后一条 BACKUP LOG 语句之后的大容量操作所修改的区的信息。
Differential Changed Map 有关每个分配单元中自最后一条 BACKUP DATABASE 语句之后更改的区的信息。
注意
日志文件不包含页,而是包含一系列日志记录。

在数据页上,数据行紧接着标头按顺序放置。页的末尾是行偏移表,对于页中的每一行,每个行偏移表都包含一个条目。每个条目记录对应行的第一个字节与页首的距离。行偏移表中的条目的顺序与页中行的顺序相反。

具有行偏移的 SQL Server 数据页

大型行支持

行不能跨页,但是行的部分可以移出行所在的页,因此行实际可能非常大。页的单个行中的最大数据量和开销是 8,060 字节 (8 KB)。但是,这不包括用 Text/Image 页类型存储的数据。包含 varcharnvarcharvarbinarysql_variant 列的表不受此限制的约束。当表中的所有固定列和可变列的行的总大小超过限制的 8,060 字节时,SQL Server 将从最大长度的列开始动态将一个或多个可变长度列移动到 ROW_OVERFLOW_DATA 分配单元中的页。每当插入或更新操作将行的总大小增大到超过限制的 8,060 字节时,将会执行此操作。将列移动到 ROW_OVERFLOW_DATA 分配单元中的页后,将在 IN_ROW_DATA 分配单元中的原始页上维护 24 字节的指针。如果后续操作减小了行的大小,SQL Server 会动态将列移回到原始数据页。有关详细信息,请参阅行溢出数据超过 8 KB

区是管理空间的基本单位。一个区是八个物理上连续的页(即 64 KB)。这意味着 SQL Server 数据库中每 MB 有 16 个区。

为了使空间分配更有效,SQL Server 不会将所有区分配给包含少量数据的表。SQL Server 有两种类型的区:

  • 统一区,由单个对象所有。区中的所有 8 页只能由所属对象使用。
  • 混合区,最多可由八个对象共享。区中八页的每页可由不同的对象所有。

通常从混合区向新表或索引分配页。当表或索引增长到 8 页时,将变成使用统一区进行后续分配。如果对现有表创建索引,并且该表包含的行足以在索引中生成 8 页,则对该索引的所有分配都使用统一区进行。

混合区和统一区

行内数据

小到中等大小的大值类型(varchar(max)nvarchar(max)varbinary(max)xml)和大型对象 (LOB) 数据类型(textntextimage)都可以存储在数据行中。该行为可以通过在 sp_tableoption 系统存储过程中使用以下两个选项来控制:用于大值类型的 large value types out of row 选项,以及用于大型对象类型的 text in row 选项。这两个选项最适用于这样的表:其中上述任意一种数据类型的数据值通常在一个单元中读/写,并且引用表的大多数语句都将引用此类数据。在行内存储的数据不一定有用,这取决于使用情况或工作负荷特征。

除非 text in row 选项设置为 ON 或特定的行内限制,否则 textntextimage 字符串都将是在数据行外存储的大型字符或二进制字符串(最多 2 GB)。数据行只包括一个 16 字节的文本指针,该指针指向一个内部指针构成的树的根节点。这些指针映射存储字符串片段的页。有关 textntextimage 字符串存储的详细信息,请参阅使用 Text 和 Image 数据

可以为包含 LOB 数据类型列的表设置 text in row 选项。还可以指定 text in row 选项限制,范围从 24 到 7,000 字节。

同样,除非 large value types out of row 选项设置为 ON,否则会尽可能将 varchar(max)nvarchar(max)varbinary(max)xml 列存储在数据行内。如果如此设置,则可以的话 SQL Server 数据库引擎将尝试容纳此特定值,否则会将其推到行外。如果 large value types out of row 设置为 ON,则上述值将存储在行外而只有 16 字节的文本指针存储在记录中。

注意
large value types out of row 设置为 OFF 时,用于大型值数据类型的最大行内存储量设置为 8,000 字节。与 text in row 选项不同,您不能指定表中列的行内限制。

将表配置为直接在数据行中存储大型值类型或大型对象数据类型时,如果存在以下情况之一,实际的列值都将存储在行内:

  • 字符串的长度小于为 textntextimage 列指定的限制值。
  • 数据行中有足够的可用空间容纳字符串。

当大型值类型或大型对象数据类型列值存储在数据行中时,数据库引擎不必访问单独的页或页集来读/写字符或二进制字符串。这便使读/写行内字符串的速度与读/写大小受限制的 varcharnvarcharvarbinary 字符串的速度大致一样。同样,当值存储在行外时,数据库引擎将引发读/写附加页。

对于大型对象数据类型,如果存储字符串所需的空间比 text in row 选项限制或行中的可用空间大,则本应存储在指针树根节点中的指针集将存储在行中。如果存在以下情况之一,指针将存储在行中:

  • 存储指针所需的空间量比 text in row 选项限制指定的空间量小。
  • 数据行中有足够的可用空间容纳指针。

当指针从根节点移至行本身时,数据库引擎不需要使用根节点。这样便可以在读/写字符串时不必访问页。从而可以提高性能。

如果使用根节点,它们将存储为 LOB 页中的一个字符串片段,并且最多可以包含 5 个内部指针。数据库引擎需要行具有 72 字节的空间来存储行内字符串的五个指针。如果 text in row 选项为 ON 或 large value types out of row 选项为 OFF 时行中没有足够的空间来容纳指针,数据库引擎可能必须分配一个 8K 的页来容纳它们。如果值的数据长度超过 40,200 字节,则需要 5 个以上的行内指针,此时只有 24 字节存储在主行中,而其他数据页被分配在 LOB 存储空间中。

当大型字符串存储在行中时,它们将与可变长度字符串的存储方式相似。数据库引擎将对列按大小以降序排序,并将值推到行外,直到剩余的列容纳在数据页 (8K) 中。

行溢出数据超过 8 KB

一个表中的每一行最多可以包含 8,060 字节。在 SQL Server 2008 中,对于包含 varcharnvarcharvarbinarysql_variant 或 CLR 用户定义类型列的表,可以放宽此限制。其中每列的长度仍必须在 8,000 字节的限制内,但是它们的总宽可以超过 8,060 字节的限制。创建和修改 varcharnvarcharvarbinarysql_variant 或 CLR 用户定义类型的列以及更新或插入数据时,此限制适用于上述列。

当合并每行超过 8060 字节的 varcharnvarcharvarbinarysql_variant 或 CLR 用户定义类型的列时,请注意下列事项:

  • 超过 8,060 字节的行大小限制可能会影响性能,因为 SQL Server 仍保持每页 8 KB 的限制。当合并 varcharnvarcharvarbinarysql_variant 或 CLR 用户定义类型的列超过此限制时,SQL Server 数据库引擎 将把最大宽度的记录列移动到 ROW_OVERFLOW_DATA 分配单元的另一页上,而在原始页上保留一个 24 字节指针。如果更新操作使记录变长,大型记录将被动态移动到另一页。如果更新操作使记录变短,记录可能会移回 IN_ROW_DATA 分配单元中的原始页。此外,执行查询和其他选择操作(例如,对包含行溢出数据的大型记录进行排序或合并)将延长处理时间,因为这些记录将同步处理,而不是 异步处理。因此,当要设计的表中包含多个 varcharnvarcharvarbinarysql_variant 或 CLR 用户定义类型的列时,请考虑可能溢出的行的百分比,以及可能查询这些溢出数据的频率。如果可能需要经常查询行溢出数据中的许多行,请考虑对表格进行规范化处理,以使某些列移动到另一个表中。然后可以在异步 JOIN 操作中执行查询。
  • 对于 varcharnvarcharvarbinarysql_variant 或 CLR 用户定义类型的列,单个列的长度仍然必须在 8000 字节的限制之内。只有它们的合并长度可以超过表的 8060 字节的行限制。
  • 其他数据类型列的和(包括 charnchar 数据)必须在 8,060 字节的行限制之内。大型对象数据也不受 8,060 字节行限制的制约。
  • 聚集索引的索引键不能包含在 ROW_OVERFLOW_DATA 分配单元中具有现有数据的 varchar 列。如果对 varchar 列创建了聚集索引,并且在 IN_ROW_DATA 分配单元中存在现有数据,则对该列执行的将数据推送到行外的后续插入或更新操作将会失败。有关分配单元的详细信息,请参阅表组织和索引组织
  • 可以包括包含行溢出数据的列,作为非聚集索引的键列或非键列。
  • 对 于使用稀疏列的表,记录大小限制为 8,018 字节。转换后的数据加上现有记录数据超过 8,018 字节时,会返回 MSSQLSERVER ERROR 576。列在稀疏和非稀疏类型之间转换时,数据库引擎会保留当前记录数据的副本。这样,记录所需的存储会临时加倍。
  • 若要获得有关可能包含行溢出数据的表或索引的信息,请使用 sys.dm_db_index_physical_stats 动态管理函数。

SQL Server调优系列基础篇

  1. SQL Server调优系列基础篇
  2. SQL Server调优系列基础篇(常用运算符总结——三种物理连接方式剖析)
  3. SQL Server调优系列基础篇(联合运算符总结)
  4. SQL Server调优系列基础篇(并行运算总结一)
  5. SQL Server调优系列基础篇(并行运算总结篇二)
  6. SQL Server调优系列基础篇(索引运算总结)
  7. SQL Server调优系列基础篇(子查询运算总结)
  8. SQL Server调优系列进阶篇(查询优化器的运行方式)
  9. SQL Server调优系列进阶篇(查询语句运行几个指标值监测)
  10. SQL Server调优系列进阶篇(深入剖析统计信息)
  11. SQL Server调优系列进阶篇(如何索引调优)
  12. SQL Server调优系列进阶篇(如何维护数据库索引)

前言

关于SQL Server调优系列是一个庞大的内容体系,非一言两语能够分析清楚,本篇先就在SQL 调优中所最常用的查询计划进行解析,力图做好基础的掌握,夯实基本功!而后再谈谈整体的语句调优。

通过本篇了解如何阅读和理解查询计划、并且列举一系列最常用的查询执行运算符。

技术准备

基于SQL Server2008R2版本,利用微软的一个更简洁的案例库(Northwind)进行解析。

一、区别不同的运算符

在所有T-SQL语句在执行的时候,都会将语句分解为一些基本的结构单元,这些结构单元统称为:运算符。每一个运算符都实现一个单独的基本操作,比 如:表扫描、索引查找、索引扫描、过滤等。每个运算符可以循环迭代,也可以延续子运算符,这样就可以组成查询树,即:查询计划。

每个T-SQL语句都会通过多种运算符进行组合形成不同的查询计划,并且这些查询计划对于结果的筛选都是有效的,但在执行的时候,SQL Server的查询优化器会自动为我们找到一个最优的。

每一个运算符都会有源数据的传入和结果数据的输出,源数据的输入可以来源于其它的运算符或者直接从数据源表中读取,经过本身的运算进行结果的输出。所以每一个运算符是独立的。互不关心的。

如下例子

SELECT COUNT(*) FROM Orders

此语句会生成两个简单的运算符

Continue reading “SQL Server调优系列基础篇”

Mono为何能跨平台?聊聊CIL(MSIL)

转载自CNBLOGS, 原作者 慕容小匹夫

前言:

其实小匹夫在U3D的开发中一直对U3D的跨平台能力很好奇。到底是什么原理使得U3D可以跨平台呢?后来发现了Mono的作用,并进一步了解到了 CIL的存在。所以,作为一个对Unity3D跨平台能力感兴趣的U3D程序猿,小匹夫如何能不关注CIL这个话题呢?那么下面各位看官就拾起语文老师教 导我们的作文口诀(WhyWhatHow),和小匹夫一起走进CIL的世界吧~

Why?

回到本文的题目,U3D或者说Mono的跨平台是如何做到的?

如果换做小匹夫或者看官你来做,应该怎么实现一套代码对应多种平台呢?

其实原理想想也简单,生活中也有很多可以参考的例子,比如下图(谁让小匹夫是做移动端开发的呢,只能物尽其用从自己身边找例子了T.T):

像这样一根线,管你是安卓还是ios都能充电。所以从这个意义上,这货也实现了跨平台。那么我们能从它身上学到什么呢?对的,那就是从一样的能源(电)到不同的平台(ios,安卓)之间需要一个中间层过度转换一下。

那么来到U3D为何能跨平台,简而言之,其实现原理在于使用了叫CIL(Common Intermediate Language通用中间语言,也叫做MSIL微软中间语言)的一种代码指令集,CIL可以在任何支持CLI(Common Language Infrastructure,通用语言基础结构)的环境中运行,就像.NET是微软对这一标准的实现,Mono则是对CLI的又一实现。由于CIL能运 行在所有支持CLI的环境中,例如刚刚提到的.NET运行时以及Mono运行时,也就是说和具体的平台或者CPU无关。这样就无需根据平台的不同而部署不 同的内容了。所以到这里,各位也应该恍然大了。代码的编译只需要分为两部分就好了嘛:

  1. 从代码本身到CIL的编译(其实之后CIL还会被编译成一种位元码,生成一个CLI assembly)
  2. 运行时从CIL(其实是CLI assembly,不过为了直观理解,不必纠结这种细节)到本地指令的即时编译(这就引出了为何U3D官方没有提供热更新的原因:在iOS平台中Mono无法使用JIT引擎,而是以Full AOT模式运行的,所以此处说的额即时编译不包括IOS

What?

上文也说了CIL是指令集,但是不是还是太模糊了呢?所以语文老师教导我们,描述一个东西时肯定要先从外貌写起。遵循老师的教导,我们不妨先通过工具来看看CIL到底长什么样。

工具就是ildasm了。下面小匹夫写一个简单的.cs看看生成的CIL代码长什么样。

C#代码:

class Class1
{
    public static void Main(string[] args)
    {
        System.Console.WriteLine("hi");
    }
}

CIL代码:

.class private auto ansi beforefieldinit Class1
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // 代码大小       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "hi"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Class1::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 代码大小       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method Class1::.ctor

} // end of class Class1

好啦。代码虽然简单,但是也能说明足够多的问题。那么和CIL的第一次亲密接触,能给我们留下什么直观的印象呢?

  1. 以“.”一个点号开头的,例如上面这份代码中的:.class、.method 。我们称之为CIL指令(directive),用于描述.NET程序集总体结构的标记。为啥需要它呢?因为你总得告诉编译器你处理的是啥吧。
  2. 貌似CIL代码中还看到了private、public这样的身影。姑且称之为CIL特性(attribute)。它的作用也很好理解,通过CIL指令并不能完全说明.NET成员和类,针对CIL指令进行补充说明成员或者类的特性的。市面上常见的还有:extends,implements等等。
  3. 每一行CIL代码基本都有的,对,那就是CIL操作码咯。小匹夫从网上找了一份汉化的操作码表放在附录部分,当然英文版的你的vs就有。

直观的印象有了,但是离我们的短期目标,说清楚(或者说介绍个大概)CIL是What,甚至是终极目标,搞明白Uniyt3D为何能跨平台还有2万4千9百里的距离。

好啦,话不多说,继续乱侃。

参照附录中的操作码表,对照可以总结出一份更易读的表格。那就是如下的表啦。

主要操作 操作数范围/条件 操作数类型 操作数
缩写 全称 含义 缩写 全称 含义 缩写 全称 含义 缩写 全称 含义
ld load 将操作数压到堆栈当中,相当于:
push ax
arg argument 参数 ? ? 操作数中的数值 .0 ? 第零个参数
.1 ? 第一个参数
.2 ? 第二个参数
.3 ? 第三个参数
.s xx (short) 参数xx
a address 操作数的地址 只有 .s xx,参见ldarg.s
loc local 局部变量 参见ldarg
fld field 字段(类的全局变量) 参见ldarg xx ? xx字段,eg:
ldfld xx
c const 常量 .i4 int 4 bytes C#里面的int,其他的类型例如short需要通过conv转换 .m1 minus 1 -1
.0 ? 0
.1 ? 1
 ……
.8 8
.s (short) 后面跟一个字节以内的整型数值(有符号的)
? ? 后面跟四个字节的整型数值
.i8 int 8 bytes C#里面的long ? ? 后面跟八个字节的整型数值
.r4 real 4 bytes C#里面的float ? ? 后面跟四个字节的浮点数值
.r8 real 8 bytes C#里面的double ? ? 后面跟八个字节的浮点数值
null null 空值(也就是0) ? ? ? ? ? ?
st store 计算堆栈的顶部弹出当前值,相当于:
pop ax
参见ld
conv convert 数值类型转换,仅仅用纯粹的数值类型间的转换,例如int/float等 ? ? ? .i1 int 1 bytes C#里面的sbyte ? ? ?
.i2 int 2 bytes C#里面的short
.i4 int 4 bytes C#里面的int
.i8 int 8 bytes C#里面的long
.r4 real 4 bytes C#里面的float
.r8 real 8 bytes C#里面的double
.u4 uint 4 bytes C#里面的uint
.u8 uint 8 bytes C#里面的ulong
b/br branch 条件和无条件跳转,相当于:
jmp/jxx label_jump
br ? ? 无条件跳转 ? ? ? ? ? 后面跟四个字节的偏移量(有符号)
.s (short) 后面跟一个字节的偏移量(有符号)
false false 值为零的时候跳转 ? ? ? 参见br
true true 值不为零的时候跳转 ? ? ?
b eq equal to 相等 ? ? ?
ne not equal to 不相等 un unsigned or unordered 无氟好的(对于整数)或者无序的(对于浮点)
gt greater than 大于
lt less than 小于
ge greater than or equal to 大于等于
le less than or equal to 小于等于
call call 调用 ? ? ? ? ? (非虚函数) ?
? ? ? virt virtual 虚函数

在此,小匹夫想请各位认真读表,然后心中默数3个数,最后看看都能发现些什么。

基于堆栈

如果是小匹夫的话,第一感觉就是基本每一条描述中都包含一个”栈“。不错,CIL是基于堆栈的,也就是说CIL的VM(mono运行时)是一个栈式机。这就意味着数据是推入堆栈,通过堆栈来操作的,而非通过CPU的寄存器来操作,这更加验证了其和具体的CPU架构没有关系。为了说明这一点,小匹夫举个例子好啦。

大学时候学单片机(大概是8086,记不清了)的时候记得做加法大概是这样的:

add eax,-2

其中的eax是啥?寄存器。所以如果CIL处理数据要通过cpu的寄存器的话,那也就不可能和cpu的架构无关了。

当然,CIL之所以是基于堆栈而非CPU的另一个原因是相比较于cpu的寄存器,操作堆栈实在太简单了。回到刚才小匹夫说的大学时候曾经学过的单片 机那门课程上,当时记得各种寄存器,各种标志位,各种。。。,而堆栈只需要简单的压栈和弹出,因此对于虚拟机的实现来说是再合适不过了。所以想要更具体的 了解CIL基于堆栈这一点,各位可以去看一下堆栈方面的内容。这里小匹夫就不拓展了。

面向对象

那么第二感觉呢?貌似附录的表中有new对象的语句呀。嗯,的确,CIL同样是面向对象的。

这意味着什么呢?那就是在CIL中你可以创建对象,调用对象的方法,访问对象的成员。而这里需要注意的就是对方法的调用。

回到上表中的右上角。对,就是对参数的操作部分。静态方法和实例方法是不同的哦~

  1. 静态方法:ldarg.0么有被占用,所以参数从ldarg.0开始。
  2. 实例方法:ldarg.0是被this占用的,也就是说实际上的参数是从ldarg.1开始的。

举个例子:假设你有一个类Murong中有一个静态方法Add(int32 a, int32 b),实现的内容就如同它的名字一样使两个数相加,所以需要2个参数。和一个实例方法TellName(string name),这个方法会告诉你传入的名字。

class  Murong
{
    public void TellName(string name)
    {
        System.Console.WriteLine(name);
    }

    public static int Add(int a, int b)
    {
       return a + b;
    }
}

静态方法的处理:

那么其中的静态方法Add的CIL代码如下:

//小匹夫注释一下。
.method public hidebysig static int32  Add(int32 a,
                                           int32 b) cil managed
{
  // 代码大小       9 (0x9)
  .maxstack  2
  .locals init ([0] int32 CS$1$0000)   //初始化局部变量列表。因为我们只返回了一个int型。所以这里声明了一个int32类型。索引为0
  IL_0000:  nop
  IL_0001:  ldarg.0     //将索引为 0 的参数加载到计算堆栈上。
  IL_0002:  ldarg.1     //将索引为 1 的参数加载到计算堆栈上。
  IL_0003:  add          //计算
  IL_0004:  stloc.0      //从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
  IL_0005:  br.s       IL_0007
  IL_0007:  ldloc.0     //将索引 0 处的局部变量加载到计算堆栈上。
  IL_0008:  ret           //返回该值
} // end of method Murong::Add

那么我们调用这个静态函数应该就是这样咯。

Murong.Add(1, 2);

对应的CIL代码为:

  IL_0001:  ldc.i4.1 //将整数1压入栈中
  IL_0002:  ldc.i4.2 //将整数2压入栈中
  IL_0003:  call       int32 Murong::Add(int32,
                                         int32)  //调用静态方法

可见CIL直接call了Murong的Add方法,而不需要一个Murong的实例。

实例方法的处理:

Murong类中的实例方法TellName()的CIL代码如下:

.method public hidebysig instance void  TellName(string name) cil managed
{
  // 代码大小       9 (0x9)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.1     //看到和静态方法的区别了吗?
  IL_0002:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0007:  nop
  IL_0008:  ret
} // end of method Murong::TellName

看到和静态方法的区别了吗?对,第一个参数对应的是ldarg.1中的参数1,而不是静态方法中的0。因为此时参数0相当于this,this是不用参与参数传递的。

那么我们再看看调用实例方法的C#代码和对应的CIL代码是如何的。

//C#
Murong murong = new Murong();
murong.TellName("chenjiadong");

CIL:

.locals init ([0] class Murong murong)   //因为C#代码中定义了一个Murong类型的变量,所以局部变量列表的索引0为该类型的引用。
//....
IL_0009:  newobj     instance void Murong::.ctor() //相比上面的静态方法的调用,此处new一个新对象,出现了instance方法。
IL_000e:  stloc.0
IL_000f:  ldloc.0
IL_0010:  ldstr      "chenjiadong" //小匹夫的名字入栈
IL_0015:  callvirt   instance void Murong::TellName(string) //实例方法的调用也有instance

到此,受制于篇幅所限(小匹夫不想写那么多字啊啊啊!)CIL是What的问题大致介绍一下。当然没有再拓展,以后小匹夫可能会再详细写一下这块。

How?

记得语文老师说过,写作文最重要的一点是要首尾呼应。既然咱们开篇就提出了U3D为何能跨平台的问题,那么接近文章的结尾咱们就再来

提问:

Q:上面的Why部分,咱们知道了U3D能跨平台是因为存在着一个能通吃的中间语言CIL,这也是所谓跨平台的前提,但是为啥CIL能通吃各大平台 呢?当然可以说CIL基于堆栈,跟你CPU怎么架构的没啥关系,但是感觉过于理论化、学术化,那还有没有通俗化、工程化的说法呢?

A:原因就是前面小匹夫提到过的,.Net运行时和Mono运行时。也就是说CIL语言其实是运行在虚拟机中的,具体到咱们的U3D也就是mono 的运行时了,换言之mono运行的其实CIL语言,CIL也并非真正的在本地运行,而是在mono运行时中运行的,运行在本地的是被编译后生成的原生代 码。当然看官博的文章,他们似乎也在开发自己的“mono”,也就是被称为脚本的未来的IL2Cpp,这种类似运行时的功能是将IL再编译成c++,再由 c++编译成原生代码,据说效率提升很可观,小匹夫也是蛮期待的。

这里为了“实现跨平台式的演示”,小匹夫用mac给各位做个测试好啦:

从C#到CIL

新建一个cs文件,然后使用mono来运行。这个cs文件内容如下:

然后咱们直接在命令行中运行这个cs文件试试~

说的很清楚,文件没有包含一个CIL映像。可见mono是不能直接运行cs文件的。假如我们把它编译成CIL呢?那么我们用mono带的mcs来编译小匹夫的Test.cs文件。

mcs Test.cs

生成了什么呢?如图:

好像没见有叫.IL的文件生成啊?反而好像多了一个.exe文件?可是没听说Mac能运行exe文件呀?可为啥又生成了.exe呢?各位看官可能要 说,小匹夫你是不是拿windows截图P的啊?嘿嘿,小匹夫可不敢。辣么真相其实就是这个exe并不是让Mac来运行的,而是留给mono运行时来运行 的,换言之这个文件的可执行代码形式是CIL的位元码形态。到此,我们完成了从C#到CIL的过程。接下来就让我们运行下刚刚的成果好啦。

mono Test.exe

结果是输出了一个大大的“Hi”。这里,就引出了下一个部分。

从CIL到Native Code

这个“HI”可是在小匹夫的MAC终端上出现的呀,那么就证明这个C#写的代码在MAC上运行的还挺“嗨”。

为啥呢?为啥C#写的代码能跑在MAC上呢?这就不得不提从CIL如何到本机原生代码的过程了。Mono提供了两种编译方式,就是我们经常能看到 的:JIT(Just-in-Time compilation,即时编译)和AOT(Ahead-of-Time,提前编译或静态编译)。这两种方式都是将CIL进一步编译成平台的原生代码。 这也是实现跨平台的最后一步。下面就分头介绍一下。

JIT即时编译:

从名字就能看的出来,即时编译,或者称之为动态编译,是在程序执行时才编译代码,解释一条语句执行一条语句,即将一条中间的托管的语句翻译成一条机 器语句,然后执行这条机器语句。但同时也会将编译过的代码进行缓存,而不是每一次都进行编译。所以可以说它是静态编译和解释器的结合体。不过你想想机器既 要处理代码的逻辑,同时还要进行编译的工作,所以其运行时的效率肯定是受到影响的。因此,Mono会有一部分代码通过AOT静态编译,以降低在程序运行时 JIT动态编译在效率上的问题。

不过一向严苛的IOS平台是不允许这种动态的编译方式的,这也是U3D官方无法给出热更新方案的一个原因。而Android平台恰恰相反,Dalvik虚拟机使用的就是JIT方案。

AOT静态编译:

其实Mono的AOT静态编译和JIT并非对立的。AOT同样使用了JIT来进行编译,只不过是被AOT编译的代码在程序运行之前就已经编译好了。当然还有一部分代码会通过JIT来进行动态编译。下面小匹夫就手动操作一下mono,让它进行一次AOT编译。

//在命令行输入
mono --aot Test.exe

结果:

从图中可以看到JIT time: 39 ms,也就是说Mono的AOT模式其实会使用到JIT,同时我们看到了生成了一个适应小匹夫的MAC的动态库Test.exe.dylib,而在Linux生成就是.so(共享库)。

AOT编译出来的库,除了包括我们的代码之外,还有被缓存的元数据信息。所以我们甚至可以只编译元数据信息而不变异代码。例如这样:

//只包含元数据的信息
mono --aot=metadata-only Test.exe

可见代码没有被包括进来。

那么简单总结一下AOT的过程:

  1. 收集要被编译的方法
  2. 使用JIT进行编译
  3. 发射(Emitting)经JIT编译过的代码和其他信息
  4. 直接生成文件或者调用本地汇编器或连接器进行处理之后生成文件。(例如上图中使用了小匹夫本地的gcc)

Full AOT

当然上文也说了,IOS平台是禁止使用JIT的,可看样子Mono的AOT模式仍然会保留一部分代码会在程序运行时动态编译。所以为了破解这个问 题,Mono提供了一个被称为Full AOT的模式。即预先对程序集中的所有CIL代码进行AOT编译生成一个本地代码映像,然后在运行时直接加载这个映像而不再使用JIT引擎。目前由于技术 或实现上的原因在使用Full AOT时有一些限制,不过这里不再多说了。以后也还会更细的分析下AOT。

总结:

好啦,写到现在也已经到了凌晨3:04分了。感觉写的内容也差不多了。那么对本文的主题U3D为何能跨平台以及CIL做个最终的总结陈词:

  1. CIL是CLI标准定义的一种可读性较低的语言。
  2. 以.NET或mono等实现CLI标准的运行环境为目标的语言要先编译成CIL,之后CIL会被编译,并且以位元码的形式存在(源代码—>中间语言的过程)。
  3. 这种位元码运行在虚拟机中(.net mono的运行时)。
  4. 这种位元码可以被进一步编译成不同平台的原生代码(中间语言—>原生代码的过程)。
  5. 面向对象
  6. 基于堆栈

附录:

名称 说明
Add 将两个值相加并将结果推送到计算堆栈上。
Add.Ovf 将两个整数相加,执行溢出检查,并且将结果推送到计算堆栈上。
Add.Ovf.Un 将两个无符号整数值相加,执行溢出检查,并且将结果推送到计算堆栈上。
And 计算两个值的按位“与”并将结果推送到计算堆栈上。
Arglist 返回指向当前方法的参数列表的非托管指针。
Beq 如果两个值相等,则将控制转移到目标指令。
Beq.S 如果两个值相等,则将控制转移到目标指令(短格式)。
Bge 如果第一个值大于或等于第二个值,则将控制转移到目标指令。
Bge.S 如果第一个值大于或等于第二个值,则将控制转移到目标指令(短格式)。
Bge.Un 当比较无符号整数值或不可排序的浮点型值时,如果第一个值大于第二个值,则将控制转移到目标指令。
Bge.Un.S 当比较无符号整数值或不可排序的浮点型值时,如果第一个值大于第二个值,则将控制转移到目标指令(短格式)。
Bgt 如果第一个值大于第二个值,则将控制转移到目标指令。
Bgt.S 如果第一个值大于第二个值,则将控制转移到目标指令(短格式)。
Bgt.Un 当比较无符号整数值或不可排序的浮点型值时,如果第一个值大于第二个值,则将控制转移到目标指令。
Bgt.Un.S 当比较无符号整数值或不可排序的浮点型值时,如果第一个值大于第二个值,则将控制转移到目标指令(短格式)。
Ble 如果第一个值小于或等于第二个值,则将控制转移到目标指令。
Ble.S 如果第一个值小于或等于第二个值,则将控制转移到目标指令(短格式)。
Ble.Un 当比较无符号整数值或不可排序的浮点型值时,如果第一个值小于或等于第二个值,则将控制转移到目标指令。
Ble.Un.S 当比较无符号整数值或不可排序的浮点值时,如果第一个值小于或等于第二个值,则将控制权转移到目标指令(短格式)。
Blt 如果第一个值小于第二个值,则将控制转移到目标指令。
Blt.S 如果第一个值小于第二个值,则将控制转移到目标指令(短格式)。
Blt.Un 当比较无符号整数值或不可排序的浮点型值时,如果第一个值小于第二个值,则将控制转移到目标指令。
Blt.Un.S 当比较无符号整数值或不可排序的浮点型值时,如果第一个值小于第二个值,则将控制转移到目标指令(短格式)。
Bne.Un 当两个无符号整数值或不可排序的浮点型值不相等时,将控制转移到目标指令。
Bne.Un.S 当两个无符号整数值或不可排序的浮点型值不相等时,将控制转移到目标指令(短格式)。
Box 将值类转换为对象引用(O 类型)。
Br 无条件地将控制转移到目标指令。
Br.S 无条件地将控制转移到目标指令(短格式)。
Break 向公共语言结构 (CLI) 发出信号以通知调试器已撞上了一个断点。
Brfalse 如果 value 为 false、空引用(Visual Basic 中的 Nothing)或零,则将控制转移到目标指令。
Brfalse.S 如果 value 为 false、空引用或零,则将控制转移到目标指令。
Brtrue 如果 value 为 true、非空或非零,则将控制转移到目标指令。
Brtrue.S 如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)。
Call 调用由传递的方法说明符指示的方法。
Calli 通过调用约定描述的参数调用在计算堆栈上指示的方法(作为指向入口点的指针)。
Callvirt 对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
Castclass 尝试将引用传递的对象转换为指定的类。
Ceq 比较两个值。如果这两个值相等,则将整数值 1 (int32) 推送到计算堆栈上;否则,将 0 (int32) 推送到计算堆栈上。
Cgt 比较两个值。如果第一个值大于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
Cgt.Un 比较两个无符号的或不可排序的值。如果第一个值大于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
Ckfinite 如果值不是有限数,则引发 ArithmeticException。
Clt 比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
Clt.Un 比较无符号的或不可排序的值 value1 和 value2。如果 value1 小于 value2,则将整数值 1 (int32 ) 推送到计算堆栈上;反之,将 0 ( int32 ) 推送到计算堆栈上。
Constrained 约束要对其进行虚方法调用的类型。
Conv.I 将位于计算堆栈顶部的值转换为 native int。
Conv.I1 将位于计算堆栈顶部的值转换为 int8,然后将其扩展(填充)为 int32。
Conv.I2 将位于计算堆栈顶部的值转换为 int16,然后将其扩展(填充)为 int32。
Conv.I4 将位于计算堆栈顶部的值转换为 int32。
Conv.I8 将位于计算堆栈顶部的值转换为 int64。
Conv.Ovf.I 将位于计算堆栈顶部的有符号值转换为有符号 native int,并在溢出时引发 OverflowException。
Conv.Ovf.I.Un 将位于计算堆栈顶部的无符号值转换为有符号 native int,并在溢出时引发 OverflowException。
Conv.Ovf.I1 将位于计算堆栈顶部的有符号值转换为有符号 int8 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I1.Un 将位于计算堆栈顶部的无符号值转换为有符号 int8 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I2 将位于计算堆栈顶部的有符号值转换为有符号 int16 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I2.Un 将位于计算堆栈顶部的无符号值转换为有符号 int16 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I4 将位于计算堆栈顶部的有符号值转换为有符号 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I4.Un 将位于计算堆栈顶部的无符号值转换为有符号 int32,并在溢出时引发 OverflowException。
Conv.Ovf.I8 将位于计算堆栈顶部的有符号值转换为有符号 int64,并在溢出时引发 OverflowException。
Conv.Ovf.I8.Un 将位于计算堆栈顶部的无符号值转换为有符号 int64,并在溢出时引发 OverflowException。
Conv.Ovf.U 将位于计算堆栈顶部的有符号值转换为 unsigned native int,并在溢出时引发 OverflowException。
Conv.Ovf.U.Un 将位于计算堆栈顶部的无符号值转换为 unsigned native int,并在溢出时引发 OverflowException。
Conv.Ovf.U1 将位于计算堆栈顶部的有符号值转换为 unsigned int8 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.U1.Un 将位于计算堆栈顶部的无符号值转换为 unsigned int8 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.U2 将位于计算堆栈顶部的有符号值转换为 unsigned int16 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.U2.Un 将位于计算堆栈顶部的无符号值转换为 unsigned int16 并将其扩展为 int32,并在溢出时引发 OverflowException。
Conv.Ovf.U4 将位于计算堆栈顶部的有符号值转换为 unsigned int32,并在溢出时引发 OverflowException。
Conv.Ovf.U4.Un 将位于计算堆栈顶部的无符号值转换为 unsigned int32,并在溢出时引发 OverflowException。
Conv.Ovf.U8 将位于计算堆栈顶部的有符号值转换为 unsigned int64,并在溢出时引发 OverflowException。
Conv.Ovf.U8.Un 将位于计算堆栈顶部的无符号值转换为 unsigned int64,并在溢出时引发 OverflowException。
Conv.R.Un 将位于计算堆栈顶部的无符号整数值转换为 float32。
Conv.R4 将位于计算堆栈顶部的值转换为 float32。
Conv.R8 将位于计算堆栈顶部的值转换为 float64。
Conv.U 将位于计算堆栈顶部的值转换为 unsigned native int,然后将其扩展为 native int。
Conv.U1 将位于计算堆栈顶部的值转换为 unsigned int8,然后将其扩展为 int32。
Conv.U2 将位于计算堆栈顶部的值转换为 unsigned int16,然后将其扩展为 int32。
Conv.U4 将位于计算堆栈顶部的值转换为 unsigned int32,然后将其扩展为 int32。
Conv.U8 将位于计算堆栈顶部的值转换为 unsigned int64,然后将其扩展为 int64。
Cpblk 将指定数目的字节从源地址复制到目标地址。
Cpobj 将位于对象(&、* 或 native int 类型)地址的值类型复制到目标对象(&、* 或 native int 类型)的地址。
Div 将两个值相除并将结果作为浮点(F 类型)或商(int32 类型)推送到计算堆栈上。
Div.Un 两个无符号整数值相除并将结果 ( int32 ) 推送到计算堆栈上。
Dup 复制计算堆栈上当前最顶端的值,然后将副本推送到计算堆栈上。
Endfilter 将控制从异常的 filter 子句转移回公共语言结构 (CLI) 异常处理程序。
Endfinally 将控制从异常块的 fault 或 finally 子句转移回公共语言结构 (CLI) 异常处理程序。
Initblk 将位于特定地址的内存的指定块初始化为给定大小和初始值。
Initobj 将位于指定地址的值类型的每个字段初始化为空引用或适当的基元类型的 0。
Isinst 测试对象引用(O 类型)是否为特定类的实例。
Jmp 退出当前方法并跳至指定方法。
Ldarg 将参数(由指定索引值引用)加载到堆栈上。
Ldarg.0 将索引为 0 的参数加载到计算堆栈上。
Ldarg.1 将索引为 1 的参数加载到计算堆栈上。
Ldarg.2 将索引为 2 的参数加载到计算堆栈上。
Ldarg.3 将索引为 3 的参数加载到计算堆栈上。
Ldarg.S 将参数(由指定的短格式索引引用)加载到计算堆栈上。
Ldarga 将参数地址加载到计算堆栈上。
Ldarga.S 以短格式将参数地址加载到计算堆栈上。
Ldc.I4 将所提供的 int32 类型的值作为 int32 推送到计算堆栈上。
Ldc.I4.0 将整数值 0 作为 int32 推送到计算堆栈上。
Ldc.I4.1 将整数值 1 作为 int32 推送到计算堆栈上。
Ldc.I4.2 将整数值 2 作为 int32 推送到计算堆栈上。
Ldc.I4.3 将整数值 3 作为 int32 推送到计算堆栈上。
Ldc.I4.4 将整数值 4 作为 int32 推送到计算堆栈上。
Ldc.I4.5 将整数值 5 作为 int32 推送到计算堆栈上。
Ldc.I4.6 将整数值 6 作为 int32 推送到计算堆栈上。
Ldc.I4.7 将整数值 7 作为 int32 推送到计算堆栈上。
Ldc.I4.8 将整数值 8 作为 int32 推送到计算堆栈上。
Ldc.I4.M1 将整数值 -1 作为 int32 推送到计算堆栈上。
Ldc.I4.S 将提供的 int8 值作为 int32 推送到计算堆栈上(短格式)。
Ldc.I8 将所提供的 int64 类型的值作为 int64 推送到计算堆栈上。
Ldc.R4 将所提供的 float32 类型的值作为 F (float) 类型推送到计算堆栈上。
Ldc.R8 将所提供的 float64 类型的值作为 F (float) 类型推送到计算堆栈上。
Ldelem 按照指令中指定的类型,将指定数组索引中的元素加载到计算堆栈的顶部。
Ldelem.I 将位于指定数组索引处的 native int 类型的元素作为 native int 加载到计算堆栈的顶部。
Ldelem.I1 将位于指定数组索引处的 int8 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.I2 将位于指定数组索引处的 int16 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.I4 将位于指定数组索引处的 int32 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.I8 将位于指定数组索引处的 int64 类型的元素作为 int64 加载到计算堆栈的顶部。
Ldelem.R4 将位于指定数组索引处的 float32 类型的元素作为 F 类型(浮点型)加载到计算堆栈的顶部。
Ldelem.R8 将位于指定数组索引处的 float64 类型的元素作为 F 类型(浮点型)加载到计算堆栈的顶部。
Ldelem.Ref 将位于指定数组索引处的包含对象引用的元素作为 O 类型(对象引用)加载到计算堆栈的顶部。
Ldelem.U1 将位于指定数组索引处的 unsigned int8 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.U2 将位于指定数组索引处的 unsigned int16 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelem.U4 将位于指定数组索引处的 unsigned int32 类型的元素作为 int32 加载到计算堆栈的顶部。
Ldelema 将位于指定数组索引的数组元素的地址作为 & 类型(托管指针)加载到计算堆栈的顶部。
Ldfld 查找对象中其引用当前位于计算堆栈的字段的值。
Ldflda 查找对象中其引用当前位于计算堆栈的字段的地址。
Ldftn 将指向实现特定方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上。
Ldind.I 将 native int 类型的值作为 native int 间接加载到计算堆栈上。
Ldind.I1 将 int8 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.I2 将 int16 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.I4 将 int32 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.I8 将 int64 类型的值作为 int64 间接加载到计算堆栈上。
Ldind.R4 将 float32 类型的值作为 F (float) 类型间接加载到计算堆栈上。
Ldind.R8 将 float64 类型的值作为 F (float) 类型间接加载到计算堆栈上。
Ldind.Ref 将对象引用作为 O(对象引用)类型间接加载到计算堆栈上。
Ldind.U1 将 unsigned int8 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.U2 将 unsigned int16 类型的值作为 int32 间接加载到计算堆栈上。
Ldind.U4 将 unsigned int32 类型的值作为 int32 间接加载到计算堆栈上。
Ldlen 将从零开始的、一维数组的元素的数目推送到计算堆栈上。
Ldloc 将指定索引处的局部变量加载到计算堆栈上。
Ldloc.0 将索引 0 处的局部变量加载到计算堆栈上。
Ldloc.1 将索引 1 处的局部变量加载到计算堆栈上。
Ldloc.2 将索引 2 处的局部变量加载到计算堆栈上。
Ldloc.3 将索引 3 处的局部变量加载到计算堆栈上。
Ldloc.S 将特定索引处的局部变量加载到计算堆栈上(短格式)。
Ldloca 将位于特定索引处的局部变量的地址加载到计算堆栈上。
Ldloca.S 将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。
Ldnull 将空引用(O 类型)推送到计算堆栈上。
Ldobj 将地址指向的值类型对象复制到计算堆栈的顶部。
Ldsfld 将静态字段的值推送到计算堆栈上。
Ldsflda 将静态字段的地址推送到计算堆栈上。
Ldstr 推送对元数据中存储的字符串的新对象引用。
Ldtoken 将元数据标记转换为其运行时表示形式,并将其推送到计算堆栈上。
Ldvirtftn 将指向实现与指定对象关联的特定虚方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上。
Leave 退出受保护的代码区域,无条件将控制转移到特定目标指令。
Leave.S 退出受保护的代码区域,无条件将控制转移到目标指令(缩写形式)。
Localloc 从本地动态内存池分配特定数目的字节并将第一个分配的字节的地址(瞬态指针,* 类型)推送到计算堆栈上。
Mkrefany 将对特定类型实例的类型化引用推送到计算堆栈上。
Mul 将两个值相乘并将结果推送到计算堆栈上。
Mul.Ovf 将两个整数值相乘,执行溢出检查,并将结果推送到计算堆栈上。
Mul.Ovf.Un 将两个无符号整数值相乘,执行溢出检查,并将结果推送到计算堆栈上。
Neg 对一个值执行求反并将结果推送到计算堆栈上。
Newarr 将对新的从零开始的一维数组(其元素属于特定类型)的对象引用推送到计算堆栈上。
Newobj 创建一个值类型的新对象或新实例,并将对象引用(O 类型)推送到计算堆栈上。
Nop 如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作。
Not 计算堆栈顶部整数值的按位求补并将结果作为相同的类型推送到计算堆栈上。
Or 计算位于堆栈顶部的两个整数值的按位求补并将结果推送到计算堆栈上。
Pop 移除当前位于计算堆栈顶部的值。
Prefix1 基础结构。此指令为保留指令。
Prefix2 基础结构。此指令为保留指令。
Prefix3 基础结构。此指令为保留指令。
Prefix4 基础结构。此指令为保留指令。
Prefix5 基础结构。此指令为保留指令。
Prefix6 基础结构。此指令为保留指令。
Prefix7 基础结构。此指令为保留指令。
Prefixref 基础结构。此指令为保留指令。
Readonly 指定后面的数组地址操作在运行时不执行类型检查,并且返回可变性受限的托管指针。
Refanytype 检索嵌入在类型化引用内的类型标记。
Refanyval 检索嵌入在类型化引用内的地址(& 类型)。
Rem 将两个值相除并将余数推送到计算堆栈上。
Rem.Un 将两个无符号值相除并将余数推送到计算堆栈上。
Ret 从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。
Rethrow 再次引发当前异常。
Shl 将整数值左移(用零填充)指定的位数,并将结果推送到计算堆栈上。
Shr 将整数值右移(保留符号)指定的位数,并将结果推送到计算堆栈上。
Shr.Un 将无符号整数值右移(用零填充)指定的位数,并将结果推送到计算堆栈上。
Sizeof 将提供的值类型的大小(以字节为单位)推送到计算堆栈上。
Starg 将位于计算堆栈顶部的值存储到位于指定索引的参数槽中。
Starg.S 将位于计算堆栈顶部的值存储在参数槽中的指定索引处(短格式)。
Stelem 用计算堆栈中的值替换给定索引处的数组元素,其类型在指令中指定。
Stelem.I 用计算堆栈上的 native int 值替换给定索引处的数组元素。
Stelem.I1 用计算堆栈上的 int8 值替换给定索引处的数组元素。
Stelem.I2 用计算堆栈上的 int16 值替换给定索引处的数组元素。
Stelem.I4 用计算堆栈上的 int32 值替换给定索引处的数组元素。
Stelem.I8 用计算堆栈上的 int64 值替换给定索引处的数组元素。
Stelem.R4 用计算堆栈上的 float32 值替换给定索引处的数组元素。
Stelem.R8 用计算堆栈上的 float64 值替换给定索引处的数组元素。
Stelem.Ref 用计算堆栈上的对象 ref 值(O 类型)替换给定索引处的数组元素。
Stfld 用新值替换在对象引用或指针的字段中存储的值。
Stind.I 在所提供的地址存储 native int 类型的值。
Stind.I1 在所提供的地址存储 int8 类型的值。
Stind.I2 在所提供的地址存储 int16 类型的值。
Stind.I4 在所提供的地址存储 int32 类型的值。
Stind.I8 在所提供的地址存储 int64 类型的值。
Stind.R4 在所提供的地址存储 float32 类型的值。
Stind.R8 在所提供的地址存储 float64 类型的值。
Stind.Ref 存储所提供地址处的对象引用值。
Stloc 从计算堆栈的顶部弹出当前值并将其存储到指定索引处的局部变量列表中。
Stloc.0 从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
Stloc.1 从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。
Stloc.2 从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中。
Stloc.3 从计算堆栈的顶部弹出当前值并将其存储到索引 3 处的局部变量列表中。
Stloc.S 从计算堆栈的顶部弹出当前值并将其存储在局部变量列表中的 index 处(短格式)。
Stobj 将指定类型的值从计算堆栈复制到所提供的内存地址中。
Stsfld 用来自计算堆栈的值替换静态字段的值。
Sub 从其他值中减去一个值并将结果推送到计算堆栈上。
Sub.Ovf 从另一值中减去一个整数值,执行溢出检查,并且将结果推送到计算堆栈上。
Sub.Ovf.Un 从另一值中减去一个无符号整数值,执行溢出检查,并且将结果推送到计算堆栈上。
Switch 实现跳转表。
Tailcall 执行后缀的方法调用指令,以便在执行实际调用指令前移除当前方法的堆栈帧。
Throw 引发当前位于计算堆栈上的异常对象。
Unaligned 指示当前位于计算堆栈上的地址可能没有与紧接的 ldind、stind、ldfld、stfld、ldobj、stobj、initblk 或 cpblk 指令的自然大小对齐。
Unbox 将值类型的已装箱的表示形式转换为其未装箱的形式。
Unbox.Any 将指令中指定类型的已装箱的表示形式转换成未装箱形式。
Volatile 指定当前位于计算堆栈顶部的地址可以是易失的,并且读取该位置的结果不能被缓存,或者对该地址的多个存储区不能被取消。
Xor 计算位于计算堆栈顶部的两个值的按位异或,并且将结果推送到计算堆栈上。