[英]Performance optimization of for-loop / switch-statement
请帮我确定以下哪些是更优化的代码?
for(int i=0;i<count;i++)
{
switch(way)
{
case 1:
doWork1(i);
break;
case 2:
doWork2(i);
break;
case 3:
doWork3(i);
break;
}
}
要么
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);
任何东西 doWork2(i);
会淹没这个; 哎呀,虚拟电话本身可能会淹没它。
如果真的,真的,真的很重要(我很难想到这里真实的情况),那么:衡量它。 在任何明显的情况下, 只有测量它的方法才是 实际的,精确的代码 - 你不能概括微微优化。
所以:
你可以这样做:
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);
}
(写在这里,代码可能不完全编译,只是为了给你这个想法......)
我会问自己优化的问题
way
? 我从哪里得到它? way
为1或2或3的概率是多少? 很明显,第一个代码片段将用于切换部分,直到i
达到计数但计数有多大? 如果它不是一个非常大的数字那么无关紧要? 如果它太大而你的运行时间非常慢那么它就没用了。 但是,正如我所说,如果你想要可读性并且可以保证计数很小,为什么不使用第一个呢? 它比第二个更容易读取,而且我喜欢的代码更少。
第二个片段,看起来很流畅但如果count是一个巨大的数字应该是首选。
实际上,尽管有一些评论,它可能会更快一些。
我们实际测试它:
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系统上构建RELEASE的结果是:
test1() took 00:00:03.8041522
test2() took 00:00:01.7916698
test2() was 2.1 times as fast.
因此,将循环移动到switch语句中会使其快速超过两次。
现在让我们通过在doWork()中添加一些代码来使它变得更加真实:
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;
}
}
}
现在我得到了这些结果:
test1() took 00:00:03.9153776
test2() took 00:00:05.3220507
test2() was 0.7 times as fast.
现在将循环放入开关是SLOWER! 这种违反直觉的结果是典型的这类事情,并说明了在尝试优化代码时应始终执行时序测试的原因。 (并且优化这样的代码通常是你甚至不应该做的事情,除非你有充分的理由怀疑存在瓶颈。你最好花时间清理你的代码。))
我做了一些其他测试,对于稍微简单的doWork()方法,test2()方法更快。 它实际上很大程度上取决于JIT编译器可以对优化做些什么。
注意:我认为我的第二个测试代码的速度差异的原因是因为JIT编译器在内联调用doWork()时可以优化掉'ref'调用,当它们不在循环中时,就像在test1()中一样; 而对于test2(),它不能(由于某种原因)。
第二种方法更有效; 无论如何,你必须完成完整的for循环。 但是在第一种方法中,你不必要地重复case语句计数次数。
假设你在这里遇到性能问题(因为在大多数情况下,交换机真的非常快):
如果您对switch语句感到困扰,我建议您在此处应用重构。
可以很容易地用策略模式替换开关(因为在for循环中没有改变切换值,所以根本不需要切换)。
真正的优化目标是那些循环,但没有上下文,很难说明可以做些什么。
这里有一些关于重构开关的更多信息(例如,关于策略模式) 关于重构开关的CodeProject文章
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.