c# - 循环从 0 开始比循环从 1 开始快?

标签 c#

看看这两个循环

 const int arrayLength = ...

版本 0
    public void RunTestFrom0()
    {
        int sum = 0;
        for (int i = 0; i < arrayLength; i++)
            for (int j = 0; j < arrayLength; j++)
                for (int k = 0; k < arrayLength; k++)
                    for (int l = 0; l < arrayLength; l++)
                        for (int m = 0; m < arrayLength; m++)
                        {
                            sum += myArray[i][j][k][l][m];
                        }
    }

版本 1
    public void RunTestFrom1()
    {
        int sum = 0;
        for (int i = 1; i < arrayLength; i++)
            for (int j = 1; j < arrayLength; j++)
                for (int k = 1; k < arrayLength; k++)
                    for (int l = 1; l < arrayLength; l++)
                        for (int m = 1; m < arrayLength; m++)
                        {
                            sum += myArray[i][j][k][l][m];
                        }
    }

版本 2
    public void RunTestFrom2()
    {
        int sum = 0;
        for (int i = 2; i < arrayLength; i++)
            for (int j = 2; j < arrayLength; j++)
                for (int k = 2; k < arrayLength; k++)
                    for (int l = 2; l < arrayLength; l++)
                        for (int m = 2; m < arrayLength; m++)
                        {
                            sum += myArray[i][j][k][l][m];
                        }
    }
arrayLength=50 的结果是(来自多次采样编译的 X64 的平均值):
  • 版本 0:0.998s(平均 0.001s 的标准误差)总循环数:312500000
  • 版本 1:1.449 秒(平均 0.000 秒的标准误差)总循环次数:282475249
  • 版本 2:0.774s(平均 0.006s 的标准误差)总循环数:254803968
  • 版本 3:1.183 秒(平均 0.001 秒的标准误差)总循环次数:229345007

  • 如果我们制作 arrayLength=45然后
  • 版本 0:0.495s(平均 0.003s 的标准误差)总循环数:184528125
  • 版本 1:0.527s(平均 0.001s 的标准误差)总循环数:164916224
  • 版本 2:0.752s(平均 0.001s 的标准误差)总循环数:147008443
  • 版本 3:0.356 秒(平均 0.000 秒的标准误差)总循环次数:130691232

  • 为什么:
  • 从 0 开始的循环比从 1 开始的循环快,但循环更多
  • 为什么循环从 2 开始的行为很奇怪?

  • 更新:
  • 我每次都运行了 10 次,(这就是平均值的标准误差的来源)
  • 我还切换了几次版本测试的顺序。没有太大区别。
  • myArray的长度每个维度的 = arrayLength ,我一开始就初始化了,所用的时间被排除在外。值为 1。所以 sum给出总循环数。
  • 编译版本是Release模式,我是从Outside VS运行的。 (封闭VS)

  • 更新2:

    现在我丢弃 myArray完全,sum++相反,并添加了 GC.Collect()
    enter image description here
        public void RunTestConstStartConstEnd()
        {
            int sum = 0;
            for (int i = constStart; i < constEnd; i++)
                for (int j = constStart; j < constEnd; j++)
                    for (int k = constStart; k < constEnd; k++)
                        for (int l = constStart; l < constEnd; l++)
                            for (int m = constStart; m < constEnd; m++)
                            {
                                sum++;
                            }
        }
    

    最佳答案

    更新

    在我看来,这似乎是 尝试优化失败的结果。抖动 ,不是编译器。总之,如果抖动可以确定下限是一个常数,它会做一些不同的事情,结果实际上更慢。我的结论的基础需要一些证明,所以请耐心等待。或者,如果您不感兴趣,请阅读其他内容!

    在测试了四种不同的设置循环下界的方法后,我得出了这个结论:

  • 在每个级别进行硬编码,如colinfang 的问题
  • 使用局部变量,通过命令行参数动态分配
  • 使用局部变量但为其分配一个常量值
  • 使用局部变量并为其分配一个常量值,但首先通过一个愚蠢的香肠研磨标识函数传递该值。这足以混淆抖动以防止其应用其恒定值“优化”。

  • 循环部分的所有四个版本的编译中间语言几乎相同。唯一的区别是在版本 1 中,下限是用命令 ldc.i4.# 加载的。 ,其中 #是 0、1、2 或 3。这代表负载常数。 (见 ldc.i4 opcode)。在所有其他版本中,下限加载 ldloc .即使在情况 3 中也是如此,编译器可以推断出 lowerBound真的是一个常数。

    由此产生的性能不是恒定的。与 OP 发现的类似,版本 1(显式常量)比版本 2(运行时参数)慢。非常有趣的是,版本 3 是 还有较慢,时间与版本 1 相当。因此,即使 IL 将下限视为变量,抖动似乎已经发现该值永远不会改变,并像版本 1 一样替换为常数,相应的性能降低。在版本 4 中,抖动无法推断出我所知道的——Confuser实际上是一个恒等函数——所以它把变量作为一个变量。结果性能与命令行参数版本 (2) 相同。

    我关于性能差异原因的理论:抖动是意识到并利用了实际处理器架构的细节。当它决定使用 0 以外的常量时,它实际上必须从不在 L2 缓存中的某个存储中获取该文字值。当它获取一个经常使用的局部变量时,它会从 L2 缓存中读取它的值,这非常快。通常,使用像已知文字整数值这样愚蠢的东西占用宝贵缓存中的空间是没有意义的。在这种情况下,我们更关心读取时间而不是存储,因此它对性能产生了不希望的影响。

    这是版本 2 的完整代码(命令行参数):
    class Program {
        static void Main(string[] args) {
            List<double> testResults = new List<double>();
            Stopwatch sw = new Stopwatch();
            int upperBound = int.Parse(args[0]) + 1;
            int tests = int.Parse(args[1]);
            int lowerBound = int.Parse(args[2]);   // THIS LINE CHANGES
            int sum = 0;
    
            for (int iTest = 0; iTest < tests; iTest++) {
                sum = 0;
                GC.Collect();
                sw.Reset();
                sw.Start();
                for (int lvl1 = lowerBound; lvl1 < upperBound; lvl1++)
                    for (int lvl2 = lowerBound; lvl2 < upperBound; lvl2++)
                        for (int lvl3 = lowerBound; lvl3 < upperBound; lvl3++)
                            for (int lvl4 = lowerBound; lvl4 < upperBound; lvl4++)
                                for (int lvl5 = lowerBound; lvl5 < upperBound; lvl5++)
                                    sum++;
                sw.Stop();
                testResults.Add(sw.Elapsed.TotalMilliseconds);
            }
    
            double avg = testResults.Average();
            double stdev = testResults.StdDev();
            string fmt = "{0,13} {1,13} {2,13} {3,13}"; string bar = new string('-', 13);
            Console.WriteLine();
            Console.WriteLine(fmt, "Iterations", "Average (ms)", "Std Dev (ms)", "Per It. (ns)");
            Console.WriteLine(fmt, bar, bar, bar, bar);
            Console.WriteLine(fmt, sum, avg.ToString("F3"), stdev.ToString("F3"),
                              ((avg * 1000000) / (double)sum).ToString("F3"));
        }
    }
    
    public static class Ext {
        public static double StdDev(this IEnumerable<double> vals) {
            double result = 0;
            int cnt = vals.Count();
            if (cnt > 1) {
                double avg = vals.Average();
                double sum = vals.Sum(d => Math.Pow(d - avg, 2));
                result = Math.Sqrt((sum) / (cnt - 1));
            }
            return result;
        }
    }
    

    对于版本 1:除删除 lowerBound 外,同上声明并替换所有 lowerBound文字实例 0 , 1 , 2 , 或 3 (分别编译和执行)。

    对于版本 3:除了用
            int lowerBound = 0; // or 1, 2, or 3
    

    对于版本 4:除了用
            int lowerBound = Ext.Confuser<int>(0); // or 1, 2, or 3
    

    哪里Confuser是:
    public static T Confuser<T>(T d) {
        decimal d1 = (decimal)Convert.ChangeType(d, typeof(decimal));
        List<decimal> L = new List<decimal>() { d1, d1 };
        decimal d2 = L.Average();
        if (d1 - d2 < 0.1m) {
            return (T)Convert.ChangeType(d2, typeof(T));
        } else {
            // This will never actually happen :)
            return (T)Convert.ChangeType(0, typeof(T));
        }
    }
    

    结果(每个测试 50 次迭代,分 5 个批次,每批次 10 个):
    1: Lower bound hard-coded in all loops: 
     Program    Iterations  Average (ms)  Std Dev (ms)  Per It. (ns)
    -------- ------------- ------------- ------------- -------------
     Looper0     345025251       267.813         1.776         0.776
     Looper1     312500000       344.596         0.597         1.103
     Looper2     282475249       311.951         0.803         1.104
     Looper3     254803968       282.710         2.042         1.109
    
    2: Lower bound supplied at command line: 
     Program    Iterations  Average (ms)  Std Dev (ms)  Per It. (ns)
    -------- ------------- ------------- ------------- -------------
      Looper     345025251       269.317         0.853         0.781
      Looper     312500000       244.946         1.434         0.784
      Looper     282475249       222.029         0.919         0.786
      Looper     254803968       201.238         1.158         0.790
    
    3: Lower bound hard-coded but copied to local variable: 
     Program    Iterations  Average (ms)  Std Dev (ms)  Per It. (ns)
    -------- ------------- ------------- ------------- -------------
    LooperX0     345025251       267.496         1.055         0.775
    LooperX1     312500000       345.614         1.633         1.106
    LooperX2     282475249       311.868         0.441         1.104
    LooperX3     254803968       281.983         0.681         1.107
    
    4: Lower bound hard-coded but ground through Confuser: 
     Program    Iterations  Average (ms)  Std Dev (ms)  Per It. (ns)
    -------- ------------- ------------- ------------- -------------
    LooperZ0     345025251       266.203         0.489         0.772
    LooperZ1     312500000       241.689         0.571         0.774
    LooperZ2     282475249       219.533         1.205         0.777
    LooperZ3     254803968       198.308         0.416         0.778
    

    这是一个庞大的数组。出于所有实际目的,您正在测试操作系统从内存中获取每个元素的值需要多长时间,而不是比较 j , k等小于 arrayLength , 以增加计数器,并增加您的总和。获取这些值的延迟与运行时或抖动本身几乎没有关系,而与整个系统上运行的其他任何事情以及堆的当前压缩和组织有很大关系。

    此外,由于您的阵列占用了如此多的空间并且被频繁访问,因此在您的某些测试迭代期间很可能正在运行垃圾收集,这会完全增加明显的 CPU 时间。

    尝试在没有数组查找的情况下进行测试——只需添加 1 ( sum++ ),然后看看会发生什么。要更彻底,请调用 GC.Collect()就在每次测试之前,以避免在循环期间进行收集。

    关于c# - 循环从 0 开始比循环从 1 开始快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9739578/

    相关文章:

    c# - 如何在 GridMvc 中设置列​​文本颜色

    c# - 为什么 Elvis (?.) 运算符不适用于异步等待?

    c# - 使用 Telerik RadGrid 导出到 Excel 会在筛选行的位置添加一个空行

    c# - 管理控制台关闭事件时出现 NullReferenceException

    c# - Mvc,授权退回授权用户

    c# - 在 Windows 窗体应用程序中使用 CaSTLe Windsor

    c# - ASP.NET web.api 帮助页面不显示任何描述

    c# - 导入属性始终为空(MEF 导入问题)

    c# - 线程化 .StartNew() 和 .Wait()

    C# 树节点移除和内存管理