[英]C# - For-loop internals
我问了一个有关循环的快速,简单的问题。
情况我突然想知道for循环的实际行为时,我正在编写一些高性能代码。 我知道我以前曾经偶然发现过这个信息,但是我一生无法再次找到此信息:/
不过,我主要关心的是限幅器。 说我们有:
for(int i = 0; i < something.awesome; i++)
{
// Do cool stuff
}
问题Some.awesome是作为内部变量存储的吗?还是循环不断检索something.awesome进行逻辑检查? 我问的原因当然是因为我需要遍历大量索引内容,而且我真的不希望每次传递都产生额外的函数调用开销。
但是,如果something.awesome仅被调用一次,那么我将回到我的快乐摇滚之下! :)
您可以使用一个简单的示例程序来检查行为:
using System;
class Program
{
static int GetUpperBound()
{
Console.WriteLine("GetUpperBound called.");
return 5;
}
static void Main(string[] args)
{
for (int i = 0; i < GetUpperBound(); i++)
{
Console.WriteLine("Loop iteration {0}.", i);
}
}
}
输出如下:
GetUpperBound called.
Loop iteration 0.
GetUpperBound called.
Loop iteration 1.
GetUpperBound called.
Loop iteration 2.
GetUpperBound called.
Loop iteration 3.
GetUpperBound called.
Loop iteration 4.
GetUpperBound called.
此行为的详细信息在C#4.0语言规范的8.3.3节中进行了描述(您可以在C:\\ Program Files \\ Microsoft Visual Studio 10.0 \\ VC#\\ Specifications \\ 1033中找到该规范):
for语句执行如下:
如果存在for初始化程序,则变量初始化程序或语句表达式将按其写入顺序执行。 此步骤仅执行一次。
如果存在条件,则对其进行评估。
如果不存在满足条件或评估得出的结果为真,则将控制权转移到嵌入语句。 当且如果控制到达嵌入式语句的终点(可能是从执行continue语句开始),则依次评估for-iterator的表达式(如果有),然后执行另一次迭代,从对上述步骤中的条件。
如果存在for条件并且评估得出false,则将控制转移到for语句的终点。
如果something.awesome是一个字段 ,则每次循环时都可能会访问该字段 ,因为循环主体中的某些内容可能会对其进行更新。 如果循环的主体足够简单,并且不调用任何方法(除了编译器内联的方法),则编译器可能能够证明将something.awesome的值放入寄存器中是安全的。 过去,编译器作家花了很多钱来做这件事。
但是,这些天来要花很长时间才能从主存储器访问一个值,但是,第一次读取该值后,它将由CPU进行引导。 从CPU高速缓存中第二次读取该值与从寄存器中读取然后从主存储器中读取该值的速度非常接近。 CPU缓存比主内存快数百倍的情况并不少见。
现在,如果something.awesome是property ,那么它实际上是一个方法调用。 每次循环时,编译器都会调用该方法。 但是,如果属性/方法仅是几行代码,则编译器可能会将其内联。 内联是指编译器直接放入方法代码的副本而不是调用方法时,因此仅返回字段值的属性的行为与上面的字段示例相同。
如果没有内联该属性,则在第一次调用该属性后,它将在CPU缓存中 。 因此,情况非常复杂,否则循环会循环很多次,第一次调用循环会花费更长的时间,可能会超过10倍。
在过去 ,这很容易,因为所有内存访问和cpu操作大约都在同一时间进行。 如今,处理器缓存可以轻松地将某些内存访问和方法调用的时间更改超过100倍 。 探查器仍倾向于假设所有内存访问都花费相同的时间! 因此,如果您进行个人资料配置,系统会告诉您进行更改,而这些更改可能对现实世界没有任何影响。
将代码更改为:
int limit = something.awesome;
for(int i = 0; i < limit; i++)
{
// Do cool stuff
}
在某些情况下会扩展它,但也会使其更加复杂。 然而
int limit = myArray.length;
for(int i = 0; i < limit; i++)
{
myArray[i[ = xyn;
}
然后慢一点
for(int i = 0; i < myArray.length; i++)
{
myArray[i[ = xyn;
}
作为.net,每次访问数组时都要检查数组的边界,并具有在循环足够简单时删除检查的逻辑。
因此,最好保持代码简单明了,直到可以证明存在问题为止。 通过花时间改进系统的总体设计,您可以获得更好的收益,如果您开始使用的代码很简单,那么这样做很容易。
每次都进行评估。 在一个简单的控制台应用程序中尝试以下操作:
public class MyClass
{
public int Value
{
get
{
Console.WriteLine("Value called");
return 3;
}
}
}
以这种方式使用:
MyClass myClass = new MyClass();
for (int i = 0; i < myClass.Value; i++)
{
}
将导致在屏幕上打印三行。
更新资料
因此,为了避免这种情况,您可以这样做:
int awesome = something.awesome;
for(int i = 0; i < awesome; i++)
{
// Do cool stuff
}
每次循环时,都会重新评估something.awesome
。
这样做会更好:
int limit = something.awesome;
for(int i = 0; i < limit; i++)
{
// Do cool stuff
}
每次编译器将检索某东西的值时,都会对其进行评估
每次都会评估条件,包括获取something.awesome
的值。 如果要避免这种情况,请将temp变量设置为something.awesome
然后将其与temp变量进行比较。
它将存储一些变量,并在使用后将其清除。
使用(int limit = something.awesome)
{
for(int i = 0; i <limiti; i ++)
{
//码。
}
}
这样,它不会每次都检查。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.