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; } |
在第一种情况下,在每次迭代中总是会有检查交换条件的开销。在第二种情况下,开销不在那里。我觉得第二种情况好多了。如果有人有其他解决办法,请帮我提出建议。
低连续值上的
如果它真的,真的,真的很重要(我努力想一个真实的场景),那么:测量它。在任何值得注意的场景中,唯一的方法就是用实际的、精确的代码来度量它——您不能概括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()则不能(出于某种原因)。
我会问自己优化的问题
很明显,第一个代码片段将用于交换部分,直到
第二个片段,看起来很难看,但如果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语句感到困扰,我建议在这里应用重构。
可以很容易地用策略模式替换开关(因为开关值在for循环中没有更改,根本不需要切换)。
真正的优化目标是那些循环,但是没有上下文。很难说该怎么办。
以下是有关重构交换机的更多信息(例如到策略模式)重构开关的代码项目文章
第二种方法更有效;无论怎样,您都必须完成完整的for循环。但是在第一个方法中,您不必要地重复case语句count次。