C语言中switch语句的开销

问题描述 投票:12回答:16

我是一个相当称职的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像素的图像,你会期望看到很多开销吗?我试过做一些测试,但我的结果到处都是,因为系统(移动设备)有各种各样的东西在后台运行,可能会扭曲结果。

另一种选择是为每种模式设置一个单独的方法,每种方法都有自己的四个循环。这显然会引入冗余代码,但效率是这里游戏的名称。

提前致谢!

c switch-statement overhead
16个回答
19
投票

Switch语句编译为连续值的跳转表和稀疏值的一堆if-else语句。在任何情况下,如果您关心性能,则不希望在内部循环中使用switch语句进行图像处理。你想改为如下。

另外,请注意我将权重计算移出内部循环(并为了实现此目的而交换了案例2的循环)。这种思维方式,将内容移出内部循环,将为您提供您想要的性能。

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..
}

1
投票

很抱歉碰到这个帖子,但在我看来,问题的转换是FAR。

在这种情况下效率的真正问题是分歧。在我看来,除法运算的所有分母都是常数(宽度,高度,最大......),这些在整个图像过程中都不会改变。如果我的猜测是正确的,那么这些是简单的变量,可以根据加载的图像进行更改,以便可以在运行时使用任何大小的图像,现在这允许加载任何图像大小,但这也意味着编译器无法优化它们进入更简单的乘法运算,如果它们被声明为“const”,它可以做到。我的建议是预先计算这些常数的反转并乘以。据我所知,乘法运算需要大约10个时钟周期,其中除法大约需要70个。每个像素增加60个周期,而上面提到的5000x5000,估计速度增加了1.5秒。 1 GHz CPU。


0
投票

取决于芯片和编译器以及代码的细节,......但这通常会实现为跳转表,这应该非常快。

顺便说一句 - 了解这种事情是一个很好的理由,可以花几周的时间在你职业生涯的某个阶段学习一些集会......


0
投票

对于速度和程序员时间,使用开关可能更好。您正在减少冗余代码,并且可能不需要新的堆栈帧。

开关非常有效,可以用于非常奇怪和令人困惑的black magic


0
投票

但效率是这里游戏的名称。

迭代图像缓冲区以计算新的像素值听起来像一个典型的令人尴尬的并行问题,在这个意义上你可能想要考虑将一些工作推入工作线程,这应该比微优化更加明显地加速你的操作如开关/案例问题。

此外,您不必每次都执行分支指令,而是可以从函数指针数组中调用函数指针,其中索引用作模式标识符。

这样你就会得到如下调用:

computeWeight[mode](pixel);

对于5000x5000像素,通过调用一系列像素而不是单个像素的函数,也可以减少函数调用开销。

您还可以使用循环展开和参考/指针传递参数,以进一步优化。


0
投票

已经给出了很多好处。我唯一能想到的就是在交换机中移动最常见的情况,并且频繁下降。

因此,如果案例4的发生频率高于案例1,那么它应该高于它:

switch (mode) {
    case 4:
        // ..
        break;
    case 1:
        // ..
        break;
}

太糟糕了,你没有使用c ++,因为那时switch语句可以用多态替换。

干杯!


0
投票

在这个线程中有很多创造性的建议,不必编写5个单独的函数。

除非您从文件或类型输入中读取“模式”,否则可以在编译时确定计算方法。作为一般规则,您不希望将计算从编译时间移动到运行时。

无论哪种方式,代码都更容易阅读,没有人会对你是否打算在第一种情况下放入break语句感到困惑。

此外,如果您在周围的代码中遇到错误,则无需查询枚举是否设置为错误的值。


0
投票

关于内循环... 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语句非常不友好。对于<4个元素,我会选择if-then-else,并确保最常见的情况是最重要的。如果第一个条件捕获了90%的情况,那么你基本上已经达到了本垒打。同样,如果某些其他条件<1%,则将其置于最后。


10
投票

如果效率比代码大小更重要,那么您应该创建冗余例程。 case语句是你在C中可以做的较低开销之一,但它不是零 - 它必须根据模式进行分支,因此需要时间。如果你真的想要最大性能,那么即使以重复循环为代价,也要将情况从循环中解脱出来。


6
投票

Switch语句尽可能高效。他们被编译成跳转表。实际上,这就是为什么切换受限制的原因:您只能编写一个开关,您可以根据固定值编译跳转表。


5
投票

与您在循环中执行的数学运算相比,交换机的开销可能很小。话虽如此,唯一可以肯定的方法是为两种不同的方法创建不同的版本,并为它们计时。


3
投票

与if / else的等价物相比,开关/外壳非常快:它通常被实现为跳转表。但是它仍然有成本。

在优化事物的同时:

1)尝试循环遍历行而不是通过列(切换x和y“表示”循环),由于高速缓存存储器管理,一个解决方案可能比另一个解决方案快得多。

2)通过(预先计算的)逆的乘法来替换所有除法将给出显着的增益,并且可能是可接受的精度损失。


2
投票

为了提高效率,你最好在循环外移动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
             ...
        }
}

它增加了函数调用开销,但它不应该太大,因为你没有将params传递给函数。我认为在性能和可读性之间进行良好的权衡。

编辑:如果您使用GCC,要摆脱函数调用,您可以使用gotolabels as values:在交换机中找到正确的标签,然后每次都跳转到它。我认为应该节省更多的周期。


1
投票

开关不应该产生任何显着的开销,它们被编译成低端的一种指针数组,那么它就是一个有效的例子:

jmp {baseaddress} + switchcasenum


1
投票

这可能取决于CPU的分支预测器有多好,以及编译器如何为交换机生成代码。对于这么少的情况,它可能会生成一个决策树,在这种情况下,正常的CPU分支预测应该能够消除大部分开销。如果它生成一个切换表,情况可能会更糟糕......

也就是说,最好的方法是找出它并查看。


1
投票

除了Jim的建议,尝试交换循环的顺序。环路交换是否适用于案例1需要测试,但我怀疑它是。您几乎总是希望在内部循环中使用x坐标以提高分页性能,因为这会使您的函数更容易在每次迭代时保持在相同的常规内存区域。具有有限资源的移动设备可能具有足够低的ram,这将强调这种差异。

© www.soinside.com 2019 - 2024. All rights reserved.