从循环中删除切换

时间:2016-11-04 21:37:35

标签: c gcc optimization loop-unrolling

我有一个内置开关的循环看起来像这样(但更复杂)。

for(int i = 0; i < N; i += inc)
{
    v = 0.0;
    switch(ConstVal)
    {
    case 2: v += A[i];
    case 1: v += A[i+k];
    case 0: v += A[i+j];
    }
    // do some stuff with v
}

ConstVal在编译时是未知的,但在初始化例程期间是固定的。有没有办法在不编译for循环的多个变体的情况下删除switch语句?鉴于x86具有间接分支,应该有一种简单的方法来内联汇编以跳转到我想要的情况,而不是每次迭代返回到循环的顶部。你会怎么做(在gcc中)?最后,这可以在不干扰编译器优化分析的情况下完成。我已经手动展开循环,但我确定还有很多优化措施正在进行,我不想打破。

我的理解是,Julia元编程功能使您可以访问解析器和抽象语法树。结合JIT,您可以解决此类问题。我认为即使没有间接分支的语义,在C中也会有一些合理的解决方法。请注意,Duff的设备不是解决方案,因为我想在每次循环迭代时返回相同的case语句。这个问题经常出现。

修改

我发现没有条件间接分支x86指令。此外,gcc内联汇编仅允许固定标签。然而,使用gcc扩展,仍然可以完成。例如,请参阅https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html#Labels-as-Values

这是怎么回事。在我的代码中,很难确定是否存在任何性能差异,但是在另一台机器上或者使用更小更简单的循环时,它可能会有所不同。

void *forswitch;
switch(ConstVal)
{
case 2: forswitch = &&C; break;
case 1: forswitch = &&B; break;
case 0: forswitch = &&A; break;
}
void *_forswitch[] = {forswitch, &&END_FOR_SWITCH};


i = 0;
{
    v = 0.0;
    C: v += _A[i];
    B: v += _A[i+k];
    A: v += _A[i+j];

    // do some stuff with v

    i += inc;
    forswitch = _forswitch[i==N];
    //forswitch = (i<N)? forswitch: &&END_FOR_SWITCH;
    goto *forswitch;
}

END_FOR_SWITCH:
return;

我已经使用我自己的实现替换了for循环,该实现基于gcc扩展,可以访问机器级别的间接分支。有几种方法可以做到这一点。第一种方法是索引一个数组,该数组根据循环索引跳转到循环的条件开始或循环结束。另一种方式(注释)是每次有条件地设置分支寄存器。编译器应使用条件移动(CMOV)替换任何分支。

此解决方案存在许多明显问题。 (1)它不便携。 (2)通过自己实现循环,它不仅更难理解代码,而且可能干扰编译器优化(例如自动循环展开)。 (3)即使没有中断,编译器也不能联合优化整个switch语句,因为它在编译时不知道实际执行哪些语句。但是,它可能能够以类似于其他人在下面的一些响应中指出的方式巧妙地重新组织交换机。通过自己手动实现交换机(与for循环相结合),我使得编译器进行任何此类优化变得更加困难,因为通过删除切换语义,我的意图被优化所掩盖。

尽管如此,如果它显着提升了性能,我仍然认为这比拥有多个代码副本更好。使用宏,可以有条件地编译非可移植扩展;这基本上可以看起来像一个普通的循环。

编辑2

我找到了一个更好的解决方案,它更便携,更有效。如果存在少量可能的运行时确定选项,则可以围绕优化函数进行包装,修复所有运行时常量,然后为每个常量副本内联函数。如果只有一个常量,则可以使用函数指针的查找表,每个函数指针都设置常量并内联函数。如果你有一个更复杂的情况,你需要一些if-elseif-else控制结构。其中一个函数可以保留所有自由变量,因此不会失去一般性。我认为这是一种编译时关闭。编译器正在做所有艰苦的工作,没有任何杂乱的宏或其他重复的代码来维护。

在我的代码中,这导致已经显着优化的代码的性能提高了10%到20%(由于各种常量的硬编码,而不是与交换机本身有任何关系)。在我的玩具示例中,更改看起来像这样。

inline void __foo(const int ConstVal)
{
    for(int i = 0; i < N; i += inc)
    {
        v = 0.0;
        switch(ConstVal)
        {
        case 2: v += A[i];
        case 1: v += A[i+k];
        case 0: v += A[i+j];
        }
        // do some stuff with v
    }
}

void foo()
{
    // this could be done with a lookup table
    switch(ConstVal)
    {
    case2: __foo(2);  break;
    case1: __foo(1);  break;
    case0: __foo(0);  break;
    }
}

通过内联__foo,编译器将消除开关以及传递的任何其他常量。当然,你会得到更多编译代码,但对于一个小的优化程序,这不应该是一个大问题。

3 个答案:

答案 0 :(得分:2)

不,我没有看到任何方法来优化switch语句。此外,它并不昂贵。因为没有break语句,所以交换机有一个&#34; fall thru&#34;趋势。它转换为:

    switch(ConstVal)
    {
    case 2: v= A[i] + A[i+k] + A[i+j]; break;
    case 1: v=        A[i+k] + A[i+j]; break;
    case 0: v=                 A[i+j]; break;
    }
    // do some stuff with v

并且我没有看到如何删除对ConstVal的依赖。

你可以在循环之前用3个循环进行切换,每个循环用于ConstVal的每个值,但这肯定看起来像丑陋的代码,具体取决于do some stuff with v的作用。

答案 1 :(得分:1)

您何时知道main.css,以及更改的频率?如果您可以重新编译一个小例程并在cribsFactory.getCribs().then(function(data) { $scope.cribs = data; }, function(error) { console.log(error); }); 更改时重新链接整个事件,那么这将解决您的问题。

话虽如此,你知道这是一个问题吗? 是ConstVal是否占执行时间的10%或更多? 人们关注非问题是很常见的。 他们知道他们应该首先介绍&#34;但他们实际上并没有这样做。 (这是&#34; ready-fire-aim&#34; :)的案例。

许多人依赖的方法是this

答案 2 :(得分:0)

  

有没有办法在不编译for循环的多个变体的情况下删除switch语句?

一般来说,制作不同的变体或者只是保持原样,它会更好。但在这种情况下,也许我们可以发明一些技巧。有点这个

switch (constVal) {
case 2:
    A1 = A;
    B1 = A + k;
    C1 = A + j;
    break;
case 1:
    A1 = big_zero_array;
    B1 = A + k;
    C1 = A + j;
    break;
case 0:
    A1 = B1 = big_zero_array;
    C1 = A + j
    break;
}

for (int i = 0; i < N; i += inc) {
    v = A1[i] + B1[i] + C1[i];
    //....
}

仍然需要额外的内存,在某些情况下可能会更慢。