关于c#:for-loop / switch-statement的性能优化

Performance optimization of for-loop / switch-statement

请帮助我确定以下哪一项是更优化的代码?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(int i=0;i<count;i++)
{
    switch(way)
    {
        case 1:
            doWork1(i);
            break;
        case 2:
            doWork2(i);
            break;
        case 3:
            doWork3(i);
            break;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
switch(way)
{
    case 1:
        for(int i=0;i<count;i++)
        {
            doWork1(i);
        }
        break;
    case 2:
        for(int i=0;i<count;i++)
        {
            doWork2(i);
        }
        break;
    case 3:
        for(int i=0;i<count;i++)
        {
            doWork3(i);
        }
        break;
}

在第一种情况下,在每次迭代中总是会有检查交换条件的开销。在第二种情况下,开销不在那里。我觉得第二种情况好多了。如果有人有其他解决办法,请帮我提出建议。


低连续值上的switch速度非常快——这种类型的跳转具有高度优化的处理能力。坦率地说,在绝大多数情况下,你所要求的不会有任何不同——在doWork2(i);中的任何东西都会淹没这一切;见鬼,虚拟电话本身可能会淹没这一切。

如果它真的,真的,真的很重要(我努力想一个真实的场景),那么:测量它。在任何值得注意的场景中,唯一的方法就是用实际的、精确的代码来度量它——您不能概括pico优化。

所以:

  • 没关系
  • 测量
  • 没关系

  • 实际上,尽管这里有一些评论,但速度可能会更快一些。

    让我们来测试一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    using System;
    using System.Diagnostics;

    namespace Demo
    {
        class Program
        {
            static void Main(string[] args)
            {
                int count = 1000000000;

                Stopwatch sw = Stopwatch.StartNew();

                for (int way = 1; way <= 3; ++way)
                    test1(count, way);

                var elapsed1 = sw.Elapsed;
                Console.WriteLine("test1() took" + elapsed1);

                sw.Restart();

                for (int way = 1; way <= 3; ++way)
                    test2(count, way);

                var elapsed2 = sw.Elapsed;
                Console.WriteLine("test2() took" + elapsed2);

                Console.WriteLine("test2() was {0:f1} times as fast.", + ((double)elapsed1.Ticks)/elapsed2.Ticks);
            }

            static void test1(int count, int way)
            {
                for (int i = 0; i < count; ++i)
                {
                    switch (way)
                    {
                        case 1: doWork1(); break;
                        case 2: doWork2(); break;
                        case 3: doWork3(); break;
                    }
                }
            }

            static void test2(int count, int way)
            {
                switch (way)
                {
                    case 1:
                        for (int i = 0; i < count; ++i)
                            doWork1();
                        break;

                    case 2:
                        for (int i = 0; i < count; ++i)
                            doWork2();
                        break;

                    case 3:
                        for (int i = 0; i < count; ++i)
                            doWork3();
                        break;
                }
            }

            static void doWork1()
            {
            }

            static void doWork2()
            {
            }

            static void doWork3()
            {
            }
        }
    }

    现在这是非常不现实的,因为dowork()方法不做任何事情。但是,它会给我们一个基准时间。

    我在Windows 7 x64系统上获得的版本生成结果是:

    1
    2
    3
    test1() took 00:00:03.8041522
    test2() took 00:00:01.7916698
    test2() was 2.1 times as fast.

    因此,将循环移入switch语句会使其速度提高两倍以上。

    现在,让我们在dowork()中添加一些代码,让它更现实一点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    using System;
    using System.Diagnostics;

    namespace Demo
    {
        class Program
        {
            static void Main(string[] args)
            {
                int count = 1000000000;

                Stopwatch sw = Stopwatch.StartNew();

                for (int way = 1; way <= 3; ++way)
                    test1(count, way);

                var elapsed1 = sw.Elapsed;
                Console.WriteLine("test1() took" + elapsed1);

                sw.Restart();

                for (int way = 1; way <= 3; ++way)
                    test2(count, way);

                var elapsed2 = sw.Elapsed;
                Console.WriteLine("test2() took" + elapsed2);

                Console.WriteLine("test2() was {0:f1} times as fast.", + ((double)elapsed1.Ticks)/elapsed2.Ticks);
            }

            static int test1(int count, int way)
            {
                int total1 = 0, total2 = 0, total3 = 0;

                for (int i = 0; i < count; ++i)
                {
                    switch (way)
                    {
                        case 1: doWork1(i, ref total1); break;
                        case 2: doWork2(i, ref total2); break;
                        case 3: doWork3(i, ref total3); break;
                    }
                }

                return total1 + total2 + total3;
            }

            static int test2(int count, int way)
            {
                int total1 = 0, total2 = 0, total3 = 0;

                switch (way)
                {
                    case 1:
                        for (int i = 0; i < count; ++i)
                            doWork1(i, ref total1);
                        break;

                    case 2:
                        for (int i = 0; i < count; ++i)
                            doWork2(i, ref total2);
                        break;

                    case 3:
                        for (int i = 0; i < count; ++i)
                            doWork3(i, ref total3);
                        break;
                }

                return total1 + total2 + total3;
            }

            static void doWork1(int n, ref int total)
            {
                total += n;
            }

            static void doWork2(int n, ref int total)
            {
                total += n;
            }

            static void doWork3(int n, ref int total)
            {
                total += n;
            }
        }
    }

    现在我得到这些结果:

    1
    2
    3
    test1() took 00:00:03.9153776
    test2() took 00:00:05.3220507
    test2() was 0.7 times as fast.

    现在,把回路放进开关要慢一点!这一违反直觉的结果是这类事情的典型结果,并演示了为什么在试图优化代码时总是要执行定时测试。(这样优化代码通常是您甚至不应该做的,除非您有充分的理由怀疑存在瓶颈。你最好把时间花在清理代码上。;)

    我做了一些其他测试,对于稍微简单的dowork()方法,test2()方法更快。这在很大程度上取决于JIT编译器可以对优化做什么。

    注意:我认为我的第二个测试代码速度差异的原因是因为JIT编译器可以在调用doWork()时优化"ref"调用,而在调用doWork()不在test1()中的循环中;而对于test2()则不能(出于某种原因)。


    我会问自己优化的问题

  • 首先,计数有多大?是1,2,10,1000000000吗?
  • 运行代码的机器有多强大?
  • 我应该写更少的代码吗?
  • 我写完后有人会读这段代码吗?如果是这样他很专业吗?
  • 我缺少什么?时间?速度?还有别的吗?
  • 什么是way?我从哪儿弄来的?概率是多少是1还是2还是3?
  • 很明显,第一个代码片段将用于交换部分,直到i达到count,但count有多大?如果不是一个很大的数字,那就不重要了?如果它太大,而且你的运行时间很慢,那么它是无用的。但是,正如我所说,如果你想要可读性并且能够保证计数很小,为什么不使用第一个呢?它比第二个更易读,而且我喜欢的代码更少。

    第二个片段,看起来很难看,但如果count是一个巨大的数字,它应该是首选的。


    你可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Func(void, int> doWork;
    switch(way)
    {
        case 1:
            doWork = doWork1;
            break;
        case 2:
            doWork = doWork2;
            break;
        case 3:
            doWork = doWork3;
            break;
    }
    for (int i=0;i<count;i++)  
    {
         doWork(i);
    }

    (写在这里,代码可能不会完全编译,只是为了给你一个想法…)


    你应该测量它,看看它是否值得优化(我很肯定它不是)。就我个人而言,我更喜欢第一个的可读性和简洁性(更少的代码,更少的错误,更多的"干燥")。

    下面是另一种更为简洁的方法:

    1
    2
    3
    4
    for(int i = 0; i < count; i++)
    {
        doAllWays(way, i); // let the method decide what to do next
    }

    所有的"方式"似乎都有所缓和,否则它们不会出现在同一个switch中。因此,首先将它们捆绑在一个方法中是有意义的,该方法执行switch


    假设您在这里遇到性能问题(因为在大多数情况下,交换机确实非常快):

    如果您对switch语句感到困扰,我建议在这里应用重构。

    可以很容易地用策略模式替换开关(因为开关值在for循环中没有更改,根本不需要切换)。

    真正的优化目标是那些循环,但是没有上下文。很难说该怎么办。

    以下是有关重构交换机的更多信息(例如到策略模式)重构开关的代码项目文章


    第二种方法更有效;无论怎样,您都必须完成完整的for循环。但是在第一个方法中,您不必要地重复case语句count次。