C 中 switch 语句的开销
Posted
技术标签:
【中文标题】C 中 switch 语句的开销【英文标题】:Overhead of a switch statement in C 【发布时间】:2010-10-29 23:16:03 【问题描述】:我是一个相当称职的 Java 程序员,对 C 语言非常陌生。我正在尝试优化一个具有四种操作模式的例程。
我遍历图像中的所有像素并根据传递的“模式”计算新的像素值。
我的问题是关于两个嵌套 for 循环中的 switch 语句的开销。我对任何有关基本 C 语句、数学和逻辑运算的相对效率的文档链接感兴趣。
代码如下;
for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
switch (mode) /* select the type of calculation */
case 0:
weight = dCentre / maxDistanceEdge;
case 1:
weight = (float)x/width;
break;
case 2:
weight = (float)y/height;
break;
case 3:
weight = dBottomLeft / maxDistanceCorner;
break;
case 4:
weight = dTopRight / maxDistanceCorner;
break;
default:
weight = 1;
break;
// Calculate the new pixel value given the weight
...
如果这是超过 5000 x 5000 像素的图像,您会期望看到很多开销吗?我尝试做一些测试,但我的结果到处都是,因为系统(移动设备)有各种各样的东西在后台运行,可能会影响结果。
另一种选择是为每种模式设置一个单独的方法,每个模式都有自己的四个循环。这显然会引入冗余代码,但效率是这里的游戏名称。
提前致谢!
Gav
【问题讨论】:
这可能更多地取决于您的编译器和编译器优化标志,而不是您的代码。你能告诉我们你正在使用哪个编译器以及你传递了哪些标志(如果有的话)? 一件事:如果你要在这个环境中做的代码不只一点点,我鼓励你弄清楚如何控制它,这样你就可以进行性能分析(关闭无线,停止后台任务等)。如果您无法获得一个稳定的平台来测试您的计时,您将永远无法确定发生了什么。 由于您总是使用 weight = a/b,因此您可以创建两个大小为 5 的数组,并使用“mode”对它们进行索引。所以看起来 weight = a1[mode] / a2[mode];那时根本没有分支。 您的意思是让您的案例 0 没有中断吗? @dss539:我对此表示怀疑,因为它设置了两次权重。这无济于事。 【参考方案1】:Switch 语句编译为连续值的跳转表和一组稀疏值的 if-else 语句。无论如何,如果您关心性能,您不希望在内部循环中使用 switch 语句进行图像处理。你想改为如下。
另外,请注意,我将权重计算移出内部循环(并为案例 2 交换了循环以实现此目的)。这种类型的思维,将东西移出内部循环,会让你从 C 中获得你想要的性能。
switch (mode) /* select the type of calculation */
case 0:
weight = dCentre / maxDistanceEdge;
for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
// Calculate the new pixel value given the weight
...
break;
case 1:
for (x = 0; x < width; x++)
weight = (float)x/width;
for (y = 0; y < height; y++)
// Calculate the new pixel value given the weight
...
break;
case 2:
// note - the loops have been swapped to get the weight calc out of the inner loop
for (y = 0; y < height; y++)
weight = (float)y/height;
for (x = 0; x < width; x++)
// Calculate the new pixel value given the weight
...
break;
case 3:
weight = dBottomLeft / maxDistanceCorner;
for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
// Calculate the new pixel value given the weight
...
break;
case 4:
weight = dTopRight / maxDistanceCorner;
for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
// Calculate the new pixel value given the weight
...
break;
default:
weight = 1;
for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
// Calculate the new pixel value given the weight
...
break;
// etc..
【讨论】:
无法保证转换成本甚至是他的瓶颈。它可能是一次内存读取、一次比较和一次跳转——所有这些都将在缓存中,并且很可能由 CPU 准确预测,因此我们谈论的是每个循环大约几个时钟周期的开销。如果“计算给定权重的新像素值”必须完全输出到主存储器,它可能已经比切换开销大一个数量级。我同意 Neil 上面的回答 - 他应该同时尝试并衡量哪个性能更好。 很好的答案,除了你埋没了关键点。你从不想在循环中做一些不会改变的事情(例如,开关的选择;6 次中的 4 次权重等) @nils:使用 -O3 还包括 -funswitch-loops 等。我会更倾向于使用它。 在情况 2 中交换循环可能弊大于利。您现在可能会乱序访问数据,这可能会完全破坏性能。额外的除法操作可能要便宜得多(或者您可能会预先计算所有权重)。话虽这么说:测量它。 只想注意原始答案中的“Jumptables”不正确。如果标签没有密集的值范围,包括 MSVC 在内的许多编译器会决定数据集是否要变成跳转表或二进制排序级联。【参考方案2】:如果效率比代码大小更重要,那么是的,您应该创建冗余例程。 case 语句是您可以在 C 中执行的开销较低的事情之一,但它不是零 - 它必须根据模式进行分支,因此需要时间。如果您真的想要最大性能,请退出循环,即使以重复循环为代价。
【讨论】:
+1,对于 5,000x5,000 的图片,您可以运行 run switch 1 次或 25,000,000 次,哪个更慢? 很容易,25,000,000 会更慢。但是我们是否在谈论每个循环慢 5 个时钟周期(完全随机选择,谁知道它有多贵。)这是 125,000,000 个时钟周期,在 1 GHz CPU 上是 0.125 秒。如果他的图像处理目前需要一秒钟,那可能会很明显。如果需要一分钟呢?或者甚至只有 5 秒?如果是这样,这不一定是他应该花费精力的地方,并且可能不值得对整体可读性造成影响。【参考方案3】:Switch 语句尽可能高效。它们被编译成一个跳转表。事实上,这就是为什么 switch 如此受限的原因:你只能编写一个 switch,你可以为它编译一个基于固定值的跳转表。
【讨论】:
是的,但它们并不是完全免费的,对于 5000 x 5000 的图像,比较/查找和跳转将完成 25,000,000 次。并不是说这个开关绝对是他的瓶颈,只是我们不应该这么快就放弃移除它,因为它很便宜 其实,switch 不需要编译成跳转表才能写出来。如果您选择了奇怪的值,编译器可能会完全决定使用普通分支而不是跳转表。看到一个开关并认为它是零成本是一种误导。我想需要测量会话或 gcc -S 会话:) 嗯,主要问题似乎是结构的效率。实际上,总的来说,他可能应该考虑多态性。 litb,首先,编译器不必将任何东西编译到跳转表;尽管如此,switch 上的约束仍然存在,因此可能编译为跳转表。 K&R 在他们的 C 书中谈到了这一点。其次,我没有说“零成本”,只是“尽可能高效”。 @Charlie,这就是我的意思。但是你在你的回答中说你不能写一个不能翻译成跳转表的开关......我试图暗示你。 wrt 到“零成本”,这并不是专门针对您的 :)【参考方案4】:与您在循环中进行的数学运算相比,切换的开销可能很小。话虽如此,唯一可以确定的方法是为两种不同的方法创建不同的版本,并为它们计时。
【讨论】:
【参考方案5】:与 if/else 的等效项相比,Switch/case 的速度非常快:它通常实现为跳转表。不过还是要付出代价的。
在优化事物时:
1) 尝试遍历行,而不是列(切换 x 和 y “for”循环),由于缓存内存管理,一种解决方案可能比另一种解决方案快得多。
2) 用(预先计算的)逆的乘法替换所有除法会给您带来显着的收益,并且可能会导致可接受的精度损失。
【讨论】:
【参考方案6】:为了提高效率,您最好将switch
移到循环之外。
我会像这样使用函数指针:
double fun0(void) return dCentre/maxDistanceEdge;
double fun1(void) return (float)x/width;
/* and so on ... */
double (*fun)(void);
switch (mode) /* select the type of calculation */
case 0: fun = fun0;
break;
case 1: fun = fun1;
break;
case 2: fun = fun2;
break;
case 3: fun = fun3;
break;
case 4: fun = fun3;
break;
default : fun = fun_default;
break;
for (x = 0; x < width; x++)
for (y = 0; y < height; y++)
weight = fun();
// Calculate the new pixel value given the weight
...
它增加了函数调用开销,但它不应该太大,因为你没有向函数传递任何参数。我认为这是性能和可读性之间的良好权衡。
编辑:如果您使用 GCC,为了摆脱函数调用,您可以使用 goto
和 labels as values: 在开关中找到正确的标签,然后每次都跳转到它。我认为它应该节省更多的周期。
【讨论】:
【参考方案7】:开关不应该产生任何显着的开销,它们在低端被编译成一种指针数组,那么这是一个有效的例子:
jmp baseaddress+switchcasenum
【讨论】:
计算跳转可能很昂贵,实际上 :) 但正如我在回答中提到的,这么小的开关最终可能会成为决策树 它们不一定编译成跳转表。所以你可以写一个不使用跳转表的开关,如果值关系太复杂而无法分层到这样的关系【参考方案8】:这可能取决于您的 CPU 的分支预测器有多好,以及您的编译器如何为开关生成代码。对于这么少的情况,它可能会生成一个决策树,在这种情况下,正常的 CPU 分支预测应该能够消除大部分开销。如果它生成一个开关表,情况可能会更糟......
也就是说,找出答案的最佳方法是对其进行分析并查看。
【讨论】:
【参考方案9】:除了 Jim 的建议之外,请尝试交换循环的顺序。循环交换是否适合案例 1 需要测试,但我怀疑它是。您几乎总是希望您的 x 坐标在您的内部循环中以提高分页性能,因为这会导致您的函数在每次迭代时更倾向于保持在相同的通用内存区域中。而资源有限的移动设备可能具有足够低的 RAM,因此会强调这种差异。
【讨论】:
【参考方案10】:很抱歉打扰了这个帖子,但在我看来,开关与问题相去甚远。
在这种情况下,效率的真正问题是划分。在我看来,除法运算的所有分母都是常数(宽度、高度、最大值......),并且这些在整个图像过程中都不会改变。如果我的猜测是正确的,那么这些是可以根据加载的图像更改的简单变量,因此可以在运行时使用任何大小的图像,现在这允许加载任何图像大小,但这也意味着编译器无法优化它们如果将它们声明为“const”,则可以执行更简单的乘法运算。我的建议是预先计算这些常数的倒数并相乘。据我记得,乘法运算大约需要 10 个时钟周期,而除法大约需要 70 个时钟周期。每像素增加 60 个周期,对于上述 5000x5000,估计速度增加了 1.5 秒1 GHz CPU。
【讨论】:
【参考方案11】:取决于芯片和编译器以及代码的细节,而且……但这通常会以跳转表的形式实现,应该很快。
顺便说一句——理解这种事情是一个很好的论据,可以在你职业生涯的某个阶段花几个星期学习一些组装......
【讨论】:
【参考方案12】:使用开关可能会更好地提高速度和程序员的时间。您正在减少冗余代码,并且可能不需要新的堆栈帧。
开关非常高效,它们可以用于非常奇怪和令人困惑的black magic。
【讨论】:
@Matt Kane。你的达夫设备的例子并不能证明开关的功效。开关只会被评估一次。也就是说,Duff 的设备可能是加快代码速度的绝佳方式。而且使用起来总是很有趣。【参考方案13】:但效率是这里的游戏名称。
迭代图像缓冲区以计算新的像素值听起来像是一个典型的令人尴尬的并行问题,从这个意义上说,您可能需要考虑将一些工作推入工作线程,这应该比微优化,例如开关/案例问题。
此外,您可以从函数指针数组中调用函数指针,而不是每次都执行分支指令,其中索引用作您的模式标识符。
这样你最终会得到如下调用:
computeWeight[mode](pixel);
对于 5000x5000 像素,函数调用开销也可以通过为一系列像素而不是单个像素调用函数来减少。
您还可以使用循环展开和通过引用/指针传递参数,以进一步优化这一点。
【讨论】:
【参考方案14】:已经给出了许多优点。我唯一能想到的补充一点,就是将最常见的情况在 switch 中上移,将最不常见的情况下移。
所以如果情况 4 发生的频率高于情况 1,那么它应该在它之上:
switch (mode)
case 4:
// ..
break;
case 1:
// ..
break;
可惜你没有使用 c++,因为这样 switch 语句可以被多态性替换。
干杯!
【讨论】:
【参考方案15】:在这个线程中有很多创造性的建议,不必编写 5 个单独的函数。
除非您从文件或键入的输入中读取“模式”,否则可以在编译时确定计算方法。作为一般规则,您不希望将计算从编译时移到运行时。
无论哪种方式,代码都会更容易阅读,并且没有人会对您是否打算在第一种情况下放入 break 语句感到困惑。
此外,当您在周围的代码中发现错误时,您不必查找枚举是否设置为错误的值。
【讨论】:
【参考方案16】:关于内部循环... 0->var 最好做 var->0 因为 var-- 触发零标志(6502 天)。这种方法也意味着“宽度”被加载到 x 中并且可以被忘记,“高度”也是如此。此外,内存中的像素通常是左->右、上->下,所以肯定有 x 作为你的内循环。
for (y = height; y--;)
for (x = width; x--;)
weight = fun();
// Calculate the new pixel value given the weight
...
另外...非常重要的是您的 switch 语句只有 2 个使用 x 或 y 的情况。其余的都是常量。
switch (mode) /* select the type of calculation */
case 0:
weight = dCentre / maxDistanceEdge;
break;
//case 1:
// weight = (float)x/width;
// break;
//case 2:
// weight = (float)y/height;
// break;
case 3:
weight = dBottomLeft / maxDistanceCorner;
break;
case 4:
weight = dTopRight / maxDistanceCorner;
break;
default:
weight = 1;
break;
所以基本上除非在循环之前计算模式 1 或 2 的权重。
... Y loop code here
if (mode == 2) weight = (float)y/height; // calc only once per Y loop
... X loop here
if (mode == 1) weight = (float)x/width; // after this all cases have filled weight
calc_pixel_using_weight(weight);
如果数据稀疏,我发现 switch 语句非常不友好。对于
【讨论】:
以上是关于C 中 switch 语句的开销的主要内容,如果未能解决你的问题,请参考以下文章