循环从0开始比循环从1开始快吗?

时间:2012-03-16 14:56:27

标签: 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.449s(平均值0.000s的标准误差)总循环次数:282475249
  • 版本2:0.774s(平均0.006秒的标准误差)总循环次数:254803968
  • 版本3:1.183s(平均0.001s的标准误差)总循环:229345007

如果我们arrayLength=45那么

  • 版本0:0.495s(平均0.003s的标准误差)总循环:184528125
  • 版本1:0.527s(平均0.001s的标准误差)总循环:164916224
  • 版本2:0.752s(平均0.001s的标准误差)总循环:147008443
  • 版本3:0.356s(平均0.000s的标准误差)总循环:130691232

为什么:

  1. 循环从0开始比循环从1开始更快但更多循环
  2. 为什么循环从2开始表现得很奇怪?
  3. 更新

    • 我每次都跑了10次,(这是平均值的标准误差来自)
    • 我也改变了版本测试的顺序几次。没什么大不同。
    • 每个维度myArray的长度= arrayLength,我在开头初始化它,并排除所用的时间。值为1.因此sum给出总循环。
    • 编译版本是Released模式,我从Outside VS运行它。 (关闭VS)

    UPDATE2:

    现在我完全放弃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++;
                            }
        }
    

4 个答案:

答案 0 :(得分:6)

<强>更新

在我看来,这是由于 jitter 而不是编译器尝试优化的结果。简而言之,如果 jitter 可以确定下限是一个常量,它会做一些不同的事情,结果实际上更慢。我的结论的基础需要一些证明,所以请耐心等待。如果你不感兴趣,或者去看别的东西!

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

  1. 在每个级别硬编码,如colinfang的问题
  2. 使用通过命令行参数动态分配的局部变量
  3. 使用局部变量但为其指定常量值
  4. 使用局部变量并为其指定一个常量值,但首先通过一个愚蠢的香肠磨削识别函数传递该值。这会使抖动混淆到足以阻止它应用其恒定值“优化”。
  5. 循环部分的所有四个版本的已编译中间语言几乎相同。唯一的区别是在版本1中,下限加载了命令ldc.i4.#,其中#是0,1,2或3.这代表加载常量。 (见ldc.i4 opcode)。在所有其他版本中,下限加载ldloc。即使在情况3中也是如此,编译器可以推断出lowerBound实际上是一个常数。

    结果表现不稳定。版本1(显式常量)比版本2(运行时参数)沿着与OP找到的类似的行慢。非常有趣的是版本3 更慢,与版本1相当。因此即使IL将下限视为变量,抖动似乎已经发现值永远不会更改,并替换版本1中的常量,并相应地降低性能。在版本4中,抖动无法推断出我所知道的内容 - Confuser实际上是一个身份函数 - 因此它将变量保留为变量。结果性能与命令行参数version(2)相同。

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

    以下是版本2(命令行arg)的完整代码:

    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个实例替换为文字012或{{ 1}}(单独编译和执行)。

    对于版本3:与上面相同,除了用

    替换lowerBound声明
    3

    对于版本4:与上面相同,除了用

    替换lowerBound声明
            int lowerBound = 0; // or 1, 2, or 3
    

    int lowerBound = Ext.Confuser<int>(0); // or 1, 2, or 3 的位置:

    Confuser

    结果(每次测试50次迭代,5次10次):

    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));
        }
    }
    

    这是一个充满活力的阵列。出于所有实际目的,您要测试操作系统从内存中获取每个元素的值所需的时间,而不是比较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,增加计数器,增加总和。获取这些值的延迟与运行时或抖动本身没什么关系,与整个系统上正在运行的其他任何事情以及堆的当前压缩和组织有很大关系。

    此外,由于您的阵列占用了大量空间并且经常被访问,因此很可能在测试迭代的某些期间运行垃圾收集,这将完全膨胀明显的CPU时间。

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

答案 1 :(得分:1)

我认为版本0更快,因为在这种情况下编译器会生成一个没有范围检查的特殊代码。请参阅http://msdn.microsoft.com/library/ms973858.aspx范围检查消除

答案 2 :(得分:0)

只是一个想法:

可能在循环中存在位移优化,因此在不均匀计数开始时需要更长的时间。

我也不知道您的处理器是否可能是不同结果的指标

答案 3 :(得分:0)

在我的头顶,也许有一个0的编译器优化 - >长度。检查构建设置(发布与调试)。

除此之外,除非你的计算机正在做其他影响基准测试的工作,否则我不确定。也许你应该改变你的基准测试以多次运行每个测试并平均结果。