请帮助我确定以下哪些是更优化的代码?
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);
中的任何事情都会淹没这一点;哎呀,虚拟呼叫本身可能会淹没它。
,真的,真的很重要(我很难在这里想到一个真实的场景),那么:衡量它。在任何明显的情况下,衡量它的唯一方法是使用实际的精确代码 - 您无法概括微微优化。
所以:
- 这不重要
- 量
- 这不重要
你可以做这样的事情:
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);
}
(写在这里,代码可能不太好编译,只是为了给你这个想法......
实际上,尽管这里有一些评论,但它可能会更快一些。
让我们实际测试一下:
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 系统上获得的发布版本的结果是:
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.
现在将环路放入交换机的速度更慢了!这种违反直觉的结果是这类事情的典型结果,并说明了为什么在尝试优化代码时应该执行时序测试。(像这样优化代码通常是你甚至不应该做的事情,除非你有充分的理由怀疑存在瓶颈。你最好花时间清理你的代码。;))
我做了一些其他测试,对于稍微简单的doWork()方法,test2()方法更快。这在很大程度上取决于 JIT 编译器可以对优化做什么。
注意:我认为我的第二个测试代码的速度差异的原因是,当 doWork() 的调用不在循环中时,JIT 编译器可以优化出 'ref' 调用,当它们不在 test1() 中时; 而对于 test2() 它不能(出于某种原因)。
我会问自己一些问题以进行优化
- 首先,计数有多大?是 1,2,10, 1000000000 吗?
- 运行代码的机器会有多强大?
- 我应该少写代码吗?
- 我写完这段代码后会有人读吗?如果是这样,如何专业是吗?
- 我缺少什么?时间?速度?别的?
- 什么是
way
?我从哪里得到它?概率是多少way
是 1 或 2 或 3?
很明显,第一个代码片段将用于开关部分,直到i
达到计数,但计数有多大?如果不是很大的数字,那也没关系?如果它太大并且运行时间非常慢,那么它是无用的。但是,正如我所说,如果您想要可读性并且可以保证计数很小,为什么不使用第一个呢?它比第二个更具可读性,并且我喜欢的代码更少。
第二个片段,看起来很丑,但如果计数是一个巨大的数字,应该首选。
你应该测量它,看看它是否值得优化(我非常确定它不是)。就个人而言,我更喜欢第一个可读性和简洁性(更少的代码,不易出错,更"干燥")。
这是另一种更简洁的方法:
for(int i = 0; i < count; i++)
{
doAllWays(way, i); // let the method decide what to do next
}
所有的"方式"似乎都是相关的,否则它们不会出现在同一switch
.因此,首先将它们捆绑在一种方法中是有意义的,该方法执行switch
。
第二种方法更有效;无论如何,您都必须完成完整的 for 循环。 但在第一种方法中,您不必要地重复 case 语句计数次数。
假设您在这里遇到性能问题(因为在大多数情况下,switch真的非常非常快):
如果你对你的switch语句感到困扰,我建议在这里应用重构。
开关可以很容易地被策略形态取代(因为开关值在 for 循环中没有更改,根本不需要切换)。
真正的优化目标是那些循环,但没有上下文很难说对此能做些什么。
以下是有关重构开关的更多信息(例如,策略模式)关于重构开关的代码项目文章