由浅入深CIL系列:6.For和Foreach的CIL结构组成以及运行效率

简介:

     本节即将新接触的CIL操作符如下:

             br.s        IL_003c    无条件地将控制转移到目标指令(短格式)

             clt          从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中

             ldloca.s   CS$5$0001 将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)

             leave.s    退出受保护的代码区域,无条件将控制转移到目标指令(缩写形式)
             constrained.   约束要对其进行虚方法调用的类型

             endfinally   将控制从异常块的 fault 或 finally 子句转移回公共语言结构 (CLI) 异常处理程序

        在C#中我们经常会遇到遍历数组、遍历List<>、遍历HashTable等情况,在本文中我们首先构造一个List<int>对象,然后通过For和Foreach来遍历它看看他们之间的CIL代码有什么区别和不同。

        首先我们贴出C#代码如下:

 


 
 
  1. class Program 
  2. static void Main(string[] args) 
  3. //初始化一个List<int> 
  4. List<int> listInt = new List<int>(); 
  5. for (int i = 0; i < 100000; i++) 
  6. listInt.Add(i + 1); 
  7. Console.WriteLine("--------------------------"); 
  8. //第一种for遍历 
  9. for (int i = 0; i < listInt.Count; i++) 
  10.  
  11. //第二种foreach遍历 
  12. foreach (int i in listInt) 
  13.  

        其次我们来看CIL代码如下所示,因为CIL代码有点儿长,设置为隐藏有需要的可以点击查看:

所有的CIL代码

 
 
  1. .method private hidebysig static void Main(string[] args) cil managed 
  2. .entrypoint 
  3. // 代码大小 123 (0x7b) 
  4. .maxstack 3 
  5. .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> listInt, 
  6. [1] int32 i, 
  7. [2] bool CS$4$0000, 
  8. [3] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0001) 
  9. IL_0000: nop 
  10. IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor() 
  11. IL_0006: stloc.0 
  12. IL_0007: ldc.i4.0 
  13. IL_0008: stloc.1 
  14. IL_0009: br.s IL_001b 
  15. IL_000b: nop 
  16. IL_000c: ldloc.0 
  17. IL_000d: ldloc.1 
  18. IL_000e: ldc.i4.1 
  19. IL_000f: add 
  20. IL_0010: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) 
  21. IL_0015: nop 
  22. IL_0016: nop 
  23. IL_0017: ldloc.1 
  24. IL_0018: ldc.i4.1 
  25. IL_0019: add 
  26. IL_001a: stloc.1 
  27. IL_001b: ldloc.1 
  28. IL_001c: ldc.i4 0x186a0 
  29. IL_0021: clt 
  30. IL_0023: stloc.2 
  31. IL_0024: ldloc.2 
  32. IL_0025: brtrue.s IL_000b 
  33. IL_0027: ldstr "--------------------------" 
  34. IL_002c: call void [mscorlib]System.Console::WriteLine(string) 
  35. IL_0031: nop 
  36. IL_0032: ldc.i4.0 
  37. IL_0033: stloc.1 
  38. IL_0034: br.s IL_003c 
  39. IL_0036: nop 
  40. IL_0037: nop 
  41. IL_0038: ldloc.1 
  42. IL_0039: ldc.i4.1 
  43. IL_003a: add 
  44. IL_003b: stloc.1 
  45. IL_003c: ldloc.1 
  46. IL_003d: ldloc.0 
  47. IL_003e: callvirt instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count() 
  48. IL_0043: clt 
  49. IL_0045: stloc.2 
  50. IL_0046: ldloc.2 
  51. IL_0047: brtrue.s IL_0036 
  52. IL_0049: nop 
  53. IL_004a: ldloc.0 
  54. IL_004b: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator() 
  55. IL_0050: stloc.3 
  56. .try 
  57. IL_0051: br.s IL_005d 
  58. IL_0053: ldloca.s CS$5$0001 
  59. IL_0055: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current() 
  60. IL_005a: stloc.1 
  61. IL_005b: nop 
  62. IL_005c: nop 
  63. IL_005d: ldloca.s CS$5$0001 
  64. IL_005f: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext() 
  65. IL_0064: stloc.2 
  66. IL_0065: ldloc.2 
  67. IL_0066: brtrue.s IL_0053 
  68. IL_0068: leave.s IL_0079 
  69. } // end .try 
  70. finally 
  71. IL_006a: ldloca.s CS$5$0001 
  72. IL_006c: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> 
  73. IL_0072: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
  74. IL_0077: nop 
  75. IL_0078: endfinally 
  76. } // end handler 
  77. IL_0079: nop 
  78. IL_007a: ret 
  79. } // end of method Program::Main 

        再次我们来分析这个CIL代码,他分为三大部分,第一部分为List的初始化,第二部分为For循环遍历List,第二部分为Foreach遍历循环,首先我们看for循环的CIL代码如下所示:


 
 
  1. //第一种for遍历 
  2. IL_0031: nop 
  3. //将整数值 0 作为 int32 推送到计算堆栈上 
  4. IL_0032: ldc.i4.0 
  5. //从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中 
  6. IL_0033: stloc.1 
  7. //无条件地将控制转移到目标指令(短格式)。 
  8. IL_0034: br.s IL_003c 
  9. IL_0036: nop 
  10. ///////////注意:这里就是循环内部需要处理的代码处,在本实例中无代码 
  11. IL_0037: nop 
  12. //将索引 1 处的局部变量加载到计算堆栈上 
  13. IL_0038: ldloc.1 
  14. //将整数值 1 作为 int32 推送到计算堆栈上 
  15. IL_0039: ldc.i4.1 
  16. //将两个值相加并将结果推送到计算堆栈上。 
  17. IL_003a: add 
  18. //从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中 
  19. IL_003b: stloc.1 
  20. //将索引 1 处的局部变量加载到计算堆栈上 
  21. IL_003c: ldloc.1 
  22. //将索引 0 处的局部变量加载到计算堆栈上 
  23. IL_003d: ldloc.0 
  24. //调用系统函数获取List数量 
  25. IL_003e: callvirt instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count() 
  26. //比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。 
  27. IL_0043: clt 
  28. //从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中 
  29. IL_0045: stloc.2 
  30. //将索引 2 处的局部变量加载到计算堆栈上 
  31. IL_0046: ldloc.2 
  32. //如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)。 
  33. IL_0047: brtrue.s IL_0036 

        下面我们来看看Foreach方式的遍历的CIL代码如下:


 
 
  1. //第二种foreach遍历 
  2. IL_0049: nop 
  3. //将索引 0 处的局部变量加载到计算堆栈上。 
  4. IL_004a: ldloc.0 
  5. //调用List<int> listInt对象的GetEnumerator()方法 
  6. //任何集合类对象都有一个GetEnumerator()方法,该方法可以返回一个实现了 IEnumerator接口的对象, 
  7. //这个返回的IEnumerator对象既不是集合类对象,也不是集合的元素类对象,它是一个独立的类对象。 
  8. //通过这个对象,可以遍历访问集合类对象中的每一个元素对象 . 
  9. IL_004b: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator() 
  10. //从计算堆栈的顶部弹出当前值并将其存储到索引 3 处的局部变量列表中 
  11. IL_0050: stloc.3 
  12. .try 
  13. //无条件地将控制转移到目标指令(短格式)。 
  14. IL_0051: br.s IL_005d 
  15. //将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。 
  16. IL_0053: ldloca.s CS$5$0001 
  17. //调用get_Current()函数返回一个Object类型。 
  18. IL_0055: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current() 
  19. //从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中 
  20. IL_005a: stloc.1 
  21. IL_005b: nop 
  22. ///////////注意:这里就是循环内部需要处理的代码处,在本实例中无代码 
  23. IL_005c: nop 
  24. //将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。 
  25. IL_005d: ldloca.s CS$5$0001 
  26. //调用MoveNext()函数运行到下一个元素 
  27. IL_005f: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext() 
  28. //从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中 
  29. IL_0064: stloc.2 
  30. //将索引 2 处的局部变量加载到计算堆栈上 
  31. IL_0065: ldloc.2 
  32. //如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)。 
  33. IL_0066: brtrue.s IL_0053 
  34. //退出受保护的代码区域,无条件将控制转移到目标指令(缩写形式)。 
  35. IL_0068: leave.s IL_0079 
  36. } // end .try 
  37. finally 
  38. //将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。 
  39. IL_006a: ldloca.s CS$5$0001 
  40. //约束要对其进行虚方法调用的类型 
  41. IL_006c: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> 
  42. //调用Dispose()将IEnumerator对象Dispose掉 
  43. IL_0072: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
  44. IL_0077: nop 
  45. //将控制从异常块的 fault 或 finally 子句转移回公共语言结构 (CLI) 异常处理程序 
  46. IL_0078: endfinally 
  47. } // end handler 

       从这里我们可以看出for方式的遍历是直接对元素集合本身的遍历,而foreach方式的遍历是对获取到元素集合的实现IEnumerator接口的对象,通过这个Current属性,调用MoveNext()函数对集合进行遍历的。

       下面我们来看看for和foreach对List对象的不同数据量级别的访问时间如下,首先我们看耗时测算代码如下:

时间耗时测算代码
 

 
 
  1. class Program 
  2. static void Main(string[] args) 
  3. //初始化一个List<int
  4. List<int> listInt = new List<int>(); 
  5. for (int i = 0; i < 1000000; i++) 
  6. listInt.Add(i + 1); 
  7. Console.WriteLine("--------------------------"); 
  8. //第一种for遍历 
  9. Stopwatch sw1 = new Stopwatch(); 
  10. sw1.Start(); 
  11. for (int i = 0; i < listInt.Count; i++) 
  12. sw1.Stop(); 
  13. Stopwatch sw2 = new Stopwatch(); 
  14. sw2.Start(); 
  15. //第二种foreach遍历 
  16. foreach (int i in listInt) 
  17. sw2.Stop(); 
  18. Console.WriteLine("当前得List<int>对象数目:"+listInt.Count.ToString()); 
  19. Console.WriteLine(@"for 的遍历消耗时间是:" + sw1.Elapsed); 
  20. Console.WriteLine(@"foreach 的遍历消耗时间是:" + sw2.Elapsed); 
  21. Console.ReadLine(); 

       首先List<int> listInt为100的耗时如下三图:

       其次List<int> listInt为10000的耗时如下三图:

       最后我们看看List<int> listInt为10000的耗时如下三图:

       结语:通过本篇文章的CIL我们知道了for和foreach在.NET环境的中间语言中是如何控制和循环的,另外也更加深入的了解for和foreach的区别。最后对于效率的比较可能和环境等有比较大的差异,大家可以不放可以自己建立一个控制台程序试试。



本文转自程兴亮 51CTO博客,原文链接:http://blog.51cto.com/chengxingliang/826737

相关文章
|
3月前
|
C++
2合1,整合C++类(Class)代码转换为MASM32代码的平台
2合1,整合C++类(Class)代码转换为MASM32代码的平台
|
6月前
|
Unix Shell 编译器
Go 中空结构有什么用法
在 Go 语言中,空结构体 struct{} 是一个非常特殊的类型,它不包含任何字段并且不占用任何内存空间。虽然听起来似乎没什么用,但空结构体在 Go 编程中实际上有着广泛的应用。本文将详细探讨空结构体的几种典型用法,并解释为何它们在特定场景下非常有用。
|
7月前
|
存储 Go
Go 语言之 Maps 详解:创建、遍历、操作和注意事项
Maps用于以键值对的形式存储数据值。Maps中的每个元素都是一个键值对。Maps是一个无序且可更改的集合,不允许重复。Maps的长度是其元素的数量。您可以使用 len() 函数来查找长度。Maps的默认值是 nil。Maps保存对底层哈希表的引用。
83 0
|
安全 Go
大白话讲讲 Go 语言的 sync.Map(二)
上一篇文章《大白话讲讲 Go 语言的 sync.Map(一)》讲到 entry 数据结构,原因是 Go 语言标准库的 map 不是线程安全的,通过加一层抽象回避这个问题……
115 1
|
存储 程序员 Go
大白话讲讲 Go 语言的 sync.Map(一)
在讲 sync.Map 之前,我们先说说什么是 map(映射)。我们每个人都有身份证号码,如果我需要从身份证号码查到对应的姓名,用 map 存储是非常合适的……
126 1
|
存储 Go
Go 编程 | 连载 12 - Slice 存储原理
Go 编程 | 连载 12 - Slice 存储原理
Go 编程 | 连载 12 - Slice 存储原理
uiu
|
编译器 Go Python
我的Go+语言初体验——GO+实现数据结构之【数组 切片 Map】(1)
我的Go+语言初体验——GO+实现数据结构之【数组 切片 Map】(1)
uiu
116 0
我的Go+语言初体验——GO+实现数据结构之【数组 切片 Map】(1)
|
Java 测试技术 Go
|
存储 移动开发 小程序
Go map 要注意这个细节,避免依赖他!
有的小伙伴没留意过 Go map 输出、遍历顺序,以为它是稳定的有序的,会在业务程序中直接依赖这个结果集顺序,结果栽了个大跟头,吃了线上 BUG。 有的小伙伴知道是无序的,但却不知道为什么,有的却理解错误?
136 0
Go map 要注意这个细节,避免依赖他!
|
Rust 前端开发 rax
Rust为什么放弃Switch结构
今天我们还是继续来聊高并发的话题,我们知道Swich分支是一个非常有用的语法,这是一个可以回溯到上世纪的Pascal、C等经典语言的分支结构,主要的作用就是判断变量的取值并将程序代码送入不同的分支,这种设计在当时的环境下非常的精妙,但是在当前最新的CPU环境下,却会带来很多意想不到的坑。
Rust为什么放弃Switch结构