C# 不同优化的无法解释的时间

C# 不同优化的无法解释的时间,c#,optimization,C#,Optimization,我正在编写一些代码,这些代码必须根据设置对大型数据集应用不同的算法。数据集很大,现实世界的时间安排表明,如果可能,我们需要对此进行优化 所选算法必须在来自大型阵列的多个子集数据上运行。因此,我决定尝试几种不同的方法: 初始化Func委托以引用所需的算法。从主循环中调用此委托 在数据上循环,并从主循环中调用适当的算法 调用一个单独的方法,该方法为每个算法实现主循环本身 在我的测试中,我让每种方法调用相同的底层方法,calculate()。(当然,实际代码会为每个算法调用不同的方法,但这里我测试的是

我正在编写一些代码,这些代码必须根据设置对大型数据集应用不同的算法。数据集很大,现实世界的时间安排表明,如果可能,我们需要对此进行优化

所选算法必须在来自大型阵列的多个子集数据上运行。因此,我决定尝试几种不同的方法:

  • 初始化
    Func
    委托以引用所需的算法。从主循环中调用此委托
  • 在数据上循环,并从主循环中调用适当的算法
  • 调用一个单独的方法,该方法为每个算法实现主循环本身
  • 在我的测试中,我让每种方法调用相同的底层方法,
    calculate()
    。(当然,实际代码会为每个算法调用不同的方法,但这里我测试的是调用算法的最快方法,而不是算法本身。)

    每个测试在循环
    ITERS
    次中调用所需的算法

    在这个测试代码中,
    datareducationalgorithm
    只是定义各种算法的枚举。除了模拟真实代码中发生的情况之外,它并没有真正被使用

    下面是我对方法(1)的测试实现。非常简单:将
    Func a
    分配给要调用的算法,然后从循环中调用它:

    private static void test1(int[] data, DataReductionAlgorithm algorithm)
    {
        Func<int[], int, int, int> a;
    
        switch (algorithm)
        {
            case DataReductionAlgorithm.Max:
                a = calculate;
                break;
    
            case DataReductionAlgorithm.Mean:
                a = calculate;
                break;
    
            default:
                a = calculate;
                break;
        }
    
        for (int i = 0; i < ITERS; ++i)
            a(data, 0, data.Length);
    }
    
    下面是测试方法的代码(3)。如果移动
    If
    测试,以在循环内选择算法。我原以为这种方法会慢一些(2),因为
    if
    测试将执行
    ITERS
    次,而不是一次:

    private static void test3(int[] data, DataReductionAlgorithm algorithm)
    {
        for (int i = 0; i < ITERS; ++i)
        {
            switch (algorithm)
            {
                case DataReductionAlgorithm.Max:
    
                    calculate(data, 0, data.Length);
                    break;
    
                case DataReductionAlgorithm.Mean:
    
                    calculate(data, 0, data.Length);
                    break;
    
                default:
    
                    calculate(data, 0, data.Length);
                    break;
            }
        }
    }
    
    以下是整个程序,以防有人想亲自尝试:

    using System;
    using System.Diagnostics;
    using System.Linq;
    
    namespace Demo
    {
        public enum DataReductionAlgorithm
        {
            Single,
            Max,
            Mean
        }
    
        internal class Program
        {
            private const int ITERS = 100000;
    
            private void run()
            {
                int[] data = Enumerable.Range(0, 10000).ToArray();
    
                Stopwatch sw = new Stopwatch();
    
                for (int trial = 0; trial < 4; ++trial)
                {
                    sw.Restart();
                    test1(data, DataReductionAlgorithm.Mean);
                    Console.WriteLine("test1: " + sw.Elapsed);
    
                    sw.Restart();
                    test2(data, DataReductionAlgorithm.Mean);
                    Console.WriteLine("test2: " + sw.Elapsed);
    
                    sw.Restart();
                    test3(data, DataReductionAlgorithm.Mean);
                    Console.WriteLine("test3: " + sw.Elapsed);
    
                    sw.Restart();
                    test4(data, DataReductionAlgorithm.Mean);
                    Console.WriteLine("test4: " + sw.Elapsed);
    
                    Console.WriteLine();
                }
            }
    
            private static void test1(int[] data, DataReductionAlgorithm algorithm)
            {
                Func<int[], int, int, int> a;
    
                switch (algorithm)
                {
                    case DataReductionAlgorithm.Max:
                        a = calculate;
                        break;
    
                    case DataReductionAlgorithm.Mean:
                        a = calculate;
                        break;
    
                    default:
                        a = calculate;
                        break;
                }
    
                for (int i = 0; i < ITERS; ++i)
                    a(data, 0, data.Length);
            }
    
            private static void test2(int[] data, DataReductionAlgorithm algorithm)
            {
                switch (algorithm)
                {
                    case DataReductionAlgorithm.Max:
    
                        for (int i = 0; i < ITERS; ++i)
                            calculate(data, 0, data.Length);
    
                        break;
    
                    case DataReductionAlgorithm.Mean:
    
                        for (int i = 0; i < ITERS; ++i)
                            calculate(data, 0, data.Length);
    
                        break;
    
                    default:
    
                        for (int i = 0; i < ITERS; ++i)
                            calculate(data, 0, data.Length);
    
                        break;
                }
            }
    
            private static void test3(int[] data, DataReductionAlgorithm algorithm)
            {
                for (int i = 0; i < ITERS; ++i)
                {
                    switch (algorithm)
                    {
                        case DataReductionAlgorithm.Max:
    
                            calculate(data, 0, data.Length);
                            break;
    
                        case DataReductionAlgorithm.Mean:
    
                            calculate(data, 0, data.Length);
                            break;
    
                        default:
    
                            calculate(data, 0, data.Length);
                            break;
                    }
                }
            }
    
            private static void test4(int[] data, DataReductionAlgorithm algorithm)
            {
                switch (algorithm)
                {
                    case DataReductionAlgorithm.Max:
    
                        iterate(ITERS, data);
                        break;
    
                    case DataReductionAlgorithm.Mean:
    
                        iterate(ITERS, data);
                        break;
    
                    default:
    
                        iterate(ITERS, data);
                        break;
                }
            }
    
            private static void iterate(int n, int[] data)
            {
                for (int i = 0; i < n; ++i)
                    calculate(data, 0, data.Length);
            }
    
            private static int calculate(int[] data, int i1, int i2)
            {
                // Just a dummy implementation.
                // Using the same algorithm for each approach to avoid differences in timings.
    
                int result = 0;
    
                for (int i = i1; i < i2; ++i)
                    result += data[i];
    
                return result;
            }
    
            private static void Main()
            {
                new Program().run();
            }
        }
    }
    
    这正是我所期望的,只是test1()比我想象的要快(可能表明调用委托是高度优化的)

    以下是x64的结果:

    test1: 00:00:00.8769743
    test2: 00:00:00.8750667
    test3: 00:00:00.5839475
    test4: 00:00:00.5853400
    

    test1()
    test2()
    发生了什么?我无法解释。
    test2()
    怎么会比
    test3()
    慢这么多

    为什么
    test4()
    的速度与
    test2()的速度不一样呢

    为什么x86和x64之间存在巨大差异

    有人能解释一下吗?速度上的差异并不是微不足道的——它可能会在10秒和15秒之间产生差异


    附录

    我已经接受了下面的答案

    但是,为了说明下面的@ UR所提到的JIT优化的脆弱性,请考虑下面的代码:

    using System;
    using System.Diagnostics;
    
    namespace Demo
    {
        internal class Program
        {
            private const int ITERS = 10000;
    
            private void run()
            {
                Stopwatch sw = new Stopwatch();
                int[] data = new int[10000];
    
                for (int trial = 0; trial < 4; ++trial)
                {
                    sw.Restart();
                    test1(data, 0);
                    var elapsed1 = sw.Elapsed;
    
                    sw.Restart();
                    test2(data, 0);
                    var elapsed2 = sw.Elapsed;
    
                    Console.WriteLine("Ratio = " + elapsed1.TotalMilliseconds / elapsed2.TotalMilliseconds);
                }
    
                Console.ReadLine();
            }
    
            private static void test1(int[] data, int x)
            {
                switch (x)
                {
                    case 0:
                    {
                        for (int i = 0; i < ITERS; ++i)
                            dummy(data);
    
                        break;
                    }
                }
            }
    
            private static void test2(int[] data, int x)
            {
                switch (x)
                {
                    case 0:
                    {
                        loop(data);
                        break;
                    }
                }
            }
    
            private static int dummy(int[] data)
            {
                int max = 0;
    
                // Also try with "int i = 1" in the loop below.
    
                for (int i = 0; i < data.Length; ++i)
                    if (data[i] > max)
                        max = data[i];
    
                return max;
            }
    
            private static void loop(int[] data)
            {
                for (int i = 0; i < ITERS; ++i)
                    dummy(data);
            }
    
            private static void Main()
            {
                new Program().run();
            }
        }
    }
    
    只需将其更改为
    i=1
    ,即可得到以下结果:

    Ratio = 1.16920209593233
    Ratio = 0.990370350435142
    Ratio = 0.991150637472754
    Ratio = 0.999941245001628
    

    有趣!:)

    我可以在x64、.NET 4.5版本上重现该问题,无需调试器

    我已经查看了为
    test2
    test3
    生成的x64。热内循环占用99%的时间。只有这个循环才重要

    对于
    test3
    calculate
    是内联的,循环边界等于数组边界。这允许JIT消除范围检查。在
    test2
    中,无法消除范围检查,因为循环边界是动态的。它们由
    inti1、inti2
    给出,它们在静态上不是有效的数组边界。只有内联可以在当前JIT中提供该信息。内联将这些值替换为
    0,data.Length

    事实并非如此。这个NET JIT在这方面并不复杂

    test3
    带内联:

    test2
    计算未内联:

    两个分支而不是一个。一个是循环测试,一个是范围检查


    我不知道为什么这里的JIT内联方式不同。内联是由启发式驱动的。

    我在我的PC上测试过它,在x86和x64上都得到了类似的结果。你用笔记本电脑做测试吗?@AlexH不,这是台式电脑。你肯定是在运行发布版本而不是在调试器下吗?因为
    calculate
    是一个非常长时间运行的函数,它应该完全控制基准测试的运行时间。由于testX函数而产生的所有差异都应该消失在噪声中。@usr但这些差异显然没有消失,如结果所示。特别是,将test2()与test4()进行比较。他们真的应该是几乎相同的时间。(在任何情况下,这如何解释x86和x64之间的差异呢?@AlexH我也在其他一些系统上尝试过这个方法,得到了相同的结果。很有趣。我想这其中的寓意是“不要在switch语句中放入循环-调用执行循环的方法”。通过这样做并将
    test2()
    转换为
    test4()
    ,我们可以获得50%的速度提升!我猜
    开关
    构造可能会引入太多的复杂性,抖动无法处理。通过将循环放入一个单独的循环中,我们将其简化到足以使抖动得到优化。如果你问我,开关是不相关的。谁知道什么样的小干扰可以抑制内联呢。你不能可靠地保护自己不受伤害。编译器启发法非常脆弱。我会尝试将数组边界计算移到循环所在的函数中。使用常量零和
    data.Length
    作为循环边界以确保安全。或者使用不安全的代码,尽管我也看到过这样的情况,即JIT无法生成好的代码。
    test1: 00:00:00.8769743
    test2: 00:00:00.8750667
    test3: 00:00:00.5839475
    test4: 00:00:00.5853400
    
    using System;
    using System.Diagnostics;
    
    namespace Demo
    {
        internal class Program
        {
            private const int ITERS = 10000;
    
            private void run()
            {
                Stopwatch sw = new Stopwatch();
                int[] data = new int[10000];
    
                for (int trial = 0; trial < 4; ++trial)
                {
                    sw.Restart();
                    test1(data, 0);
                    var elapsed1 = sw.Elapsed;
    
                    sw.Restart();
                    test2(data, 0);
                    var elapsed2 = sw.Elapsed;
    
                    Console.WriteLine("Ratio = " + elapsed1.TotalMilliseconds / elapsed2.TotalMilliseconds);
                }
    
                Console.ReadLine();
            }
    
            private static void test1(int[] data, int x)
            {
                switch (x)
                {
                    case 0:
                    {
                        for (int i = 0; i < ITERS; ++i)
                            dummy(data);
    
                        break;
                    }
                }
            }
    
            private static void test2(int[] data, int x)
            {
                switch (x)
                {
                    case 0:
                    {
                        loop(data);
                        break;
                    }
                }
            }
    
            private static int dummy(int[] data)
            {
                int max = 0;
    
                // Also try with "int i = 1" in the loop below.
    
                for (int i = 0; i < data.Length; ++i)
                    if (data[i] > max)
                        max = data[i];
    
                return max;
            }
    
            private static void loop(int[] data)
            {
                for (int i = 0; i < ITERS; ++i)
                    dummy(data);
            }
    
            private static void Main()
            {
                new Program().run();
            }
        }
    }
    
    Ratio = 1.52235829774506
    Ratio = 1.50636405328076
    Ratio = 1.52291602053827
    Ratio = 1.52803278744701
    
    Ratio = 1.16920209593233
    Ratio = 0.990370350435142
    Ratio = 0.991150637472754
    Ratio = 0.999941245001628