8

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

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

无缓存:

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

}

使用缓存:

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

}

根据文章“缓存”,值会Length在循环中删除一个操作,因为与访问对象成员相比,访问局部变量更快。与简单地访问成员相比,声明局部变量实际上更快吗?编译器是否会注意到这一点并自动缓存该值?在访问成员时声明局部变量是否有任何负面影响?

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

从评论来看,访问数组长度似乎非常快。可以说我改用了IList<>。缓存 的值Count会比每次迭代检索它更快吗?

4

4 回答 4

2

只是为了提供不同的答案。在某些特定情况下,缓存长度值可能会对您有所帮助。

就我而言,我们使用带有selenium的示例代码进行 ui 测试。代码如下所示:

for (int i = 0; i < Grid.VisibleRows.Length; i++) {
    // do something
}

我们的网格只是一个代表 html 表格的类。VisibleRows 得到一个IWebElement[]然后使用 Length 属性。每次迭代都意味着去 UI 并获取所有行。

当然,另一种实现可能是只进入 UI 一次并获取长度(而不是计算内存中的行数),但仍然将代码移出 for 循环意味着性能的提高 - 我将往返次数减少到UI 从多到只有一个。

所以代码现在看起来像这样:

var rowsLength = Grid.VisibleRows.Length;
 for (int i = 0; i < rowsLength ; i++) {
        // do something
    }

对我们来说,手动缓存很有效。使用秒表,我们检查了我们能够将这部分代码的时间从 32993 毫秒减少到 12020 毫秒

于 2017-12-04T18:42:15.417 回答
1

我曾经尝试过缓存与 array.Length。Array.Length更快。我认为,那是因为虚拟机的内部结构和“安全性”。当您使用.Length符号时,它认为它永远不会溢出数组。对于变量,它是未知的,它会进行额外的测试。汇编代码看起来是一种方式,但虚拟机的内部行为是另一回事。

但另一方面,您正在进行过早优化。

于 2013-05-19T07:51:18.733 回答
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 );
    }
}

这是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

您的问题的关键答案是,它是将数组推送到堆栈中的位置 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()

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

你最好不要担心需要几毫秒的事情——比如分块数据库调用,或者优化 SQL 查询以使其更快等等,而不是试图削减单个 IL 操作。

于 2013-05-19T07:44:15.010 回答
-1

我怀疑它会让你的代码更快,实际上可能会让它变慢。获取数组的长度真的很便宜(可能是 L1 命中),并且预先计算长度可能会弄乱 JIT 的边界检查消除。

在 C# 中,写:array[i] 实际上更像是写: if (i >= array.Length) throw...; 数组[i]

CLR 编写者花了很多时间使 JIT 真正擅长在不必要时消除这些检查。以第二种风格编写循环可能会使 CLR 无法消除检查。

于 2013-05-19T07:46:39.717 回答