在`for`循环中缓存变量

jdu*_*tor 6 c# for-loop

我一直在阅读关于Javascript性能提升的提示,并且一个提示说要在循环比较器语句中缓存所有变量(不会改变),我想知道这是否也适用于.NET.

假设我有一个简单的for循环,下面哪个会更快或者它们是否相同?

没有缓存:

for (int i = 0; i < someArray.Length; i++)
{

}
Run Code Online (Sandbox Code Playgroud)

使用缓存:

for (int i = 0, count = someArray.Length; i < count; i++)
{

}
Run Code Online (Sandbox Code Playgroud)

根据文章"缓存" Length,在循环中删除一个操作的值,因为与访问对象的成员相比,它更快地访问局部变量.与简单地访问成员相比,它实际上更快地声明局部变量吗?编译器是否接受此操作并自动缓存该值?声明局部变量而不是访问该成员是否有任何负面影响?

虽然速度可能是这里的一个关键因素,但它不是唯一的因素.我的下一个问题可能是哪一个更有效率.哪个使用较少的内存分配?哪个执行较少的堆栈操作?等等...

从评论来看,似乎访问数组的长度非常快.让我们说我用了一个IList<>.缓存值Count是否比每次迭代检索它更快?

Ada*_*lls 0

在编译语言中,您所做的只是过早优化。我认为解释性语言可能会节省一点,但即使在那里,对于(根据我的经验)编写 for 循环的不寻常方式,您的回报也会很小。

直接回答 C# 的问题,不,编译器不会通过缓存优化任何内容。我可以在循环期间非常轻松地创建一个具有新长度的新数组。因此,每次评估停止条件时都会加载数组长度。或者更糟糕的是,我可能没有使用“传统”样式的停止条件,并且可能需要评估函数才能知道停止。

也就是说,这是一个简单的程序:

static void Main( string[] args ) {
    int[] integers = new int[] { 1, 2, 3, 4, 5 };

    for( int i = 0; i < integers.Length; i++ ) {
        Console.WriteLine( i );
    }
}
Run Code Online (Sandbox Code Playgroud)

这是 IL(删除了 nops):

IL_000d:  call       void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array,
                                                                                                     valuetype [mscorlib]System.RuntimeFieldHandle)
IL_0012:  stloc.0
IL_0013:  ldc.i4.0
IL_0014:  stloc.1
IL_0015:  br.s       IL_0024
IL_0018:  ldloc.1
IL_0019:  call       void [mscorlib]System.Console::WriteLine(int32)
IL_0020:  ldloc.1
IL_0021:  ldc.i4.1
IL_0022:  add
IL_0023:  stloc.1
IL_0024:  ldloc.1
IL_0025:  ldloc.0
IL_0026:  ldlen
IL_0027:  conv.i4
IL_0028:  clt
IL_002a:  stloc.2
IL_002b:  ldloc.2
IL_002c:  brtrue.s   IL_0017
Run Code Online (Sandbox Code Playgroud)

这里问题的关键答案是,它将数组推入堆栈中的位置 0,然后在 IL_0026 期间执行调用以获取数组的长度,然后 IL_0028 执行小于比较,最后转到 IL_0017如果评价属实的话。

通过缓存数组的长度,您所节省的只是 ldlen 和 stloc 调用。ldlen 指令应该很快,因为获取数组的长度不会浪费太多时间。

编辑:

与列表的主要区别是这条指令:

IL_002b:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
Run Code Online (Sandbox Code Playgroud)

callvirt 会占用更多时间,但该函数实际上所做的只是返回一个私有变量。

与尝试减少单个 IL 操作相比,您最好不要担心需要几毫秒的事情,例如分块数据库调用或优化 SQL 查询以使其更快等。

  • 请注意,无论如何,IL 并没有讲述整个故事。我希望 JIT 能够发现这种令人难以置信的常见模式,并针对数组情况进一步优化它,如果它可以知道数组变量在循环期间不会改变(引用不同的数组)。 (10认同)