C ++:循环优化和循环展开(循环或不循环)

时间:2013-12-02 04:48:26

标签: c++ optimization for-loop loop-unrolling

更新

这个讨论比我预期的要进一步,所以当我将这个问题突然出现在脑海中的时候,我正在用我正在处理的代码更新这个。这是一个8到16行代码的决定,以确定谁是我的c ++课程入门的井字游戏的赢家。

注意:这是为了与课程保持同步,

注意2:令牌是字符x或o或'')

这是一个优化问题。如果这是重复,我道歉但我无法在其他地方找到答案。

基本上,它归结为以下代码是否会更好地循环:

    char CheckForWinner() {

    //returns the token of the player that satisfies one of the winning requirements
    if (Square[0][0] == Square[0][1] && Square[0][0] == Square[0][2] ) { //If all three tokens in the first row are the same
        return Square[0][0]; //Return the token
    } else if (Square[1][0] == Square[1][1] && Square[1][0] == Square[1][2] ) { //Check the next row
        return Square[1][0]; //Return the token
    } else if (Square[2][0] == Square[2][1] && Square[2][0] == Square[2][2] ) {
        return Square[2][0];
    } else if (Square[0][0] == Square[1][0] && Square[0][0] == Square[2][0] ) { //If no rows satisfy conditions, check columns
        return Square[0][0]; //Return the token
    } else if (Square[0][1] == Square[1][1] && Square[0][1] == Square[2][1] ) { 
        return Square[0][1];
    } else if (Square[0][2] == Square[1][2] && Square[0][2] == Square[2][2] ) { 
        return Square[0][2];
    } else if (Square[0][0] == Square[1][1] && Square[0][0] == Square[2][2] ) { //finally, check diagonals
        return Square[0][0];
    } else if (Square[0][2] == Square[1][1] && Square[0][2] == Square[2][0] ) {
        return Square[0][2];
    }

    return ' ';
}

这对他们只是输入100条cout线的系统或多或少负担?

我很好奇,因为我们不仅要执行100个cout行,而且还要为内存分配一个新变量,并强制计算机处理100个数学方程式并输出数据。

我可以理解编译器可能提供某种程度的优化,但我有兴趣在更一般的层面上知道。首先,我使用VisualStudio 2012或MingGW(g ++)进行编译。

4 个答案:

答案 0 :(得分:5)

关于展开循环的所有100次迭代是否有效,没有一个单一的答案。

对于没有代码缓存的“较小”系统,展开所有100次迭代的机会非常好,至少在执行速度方面是这样。另一方面,一个小到足以使其CPU没有缓存的系统通常会在其他资源中受到足够的限制,因此这样做是非常不可取的。

如果系统确实有一个缓存,那么展开循环的所有100次迭代往往会导致执行速度变慢的可能性非常大。循环本身的开销几乎肯定比重新获取基本相同的代码100次更少。

在典型情况下,当展开少量循环的迭代(但通常少于100次迭代)时,循环展开最有效。在典型情况下,您会看到大约4到16次迭代的广泛平台被展开。

然而,正如许多人首先尝试优化的典型情况一样,我猜你真的在完全错误的方向。如果你想优化那个循环,很可能(到目前为止)最大的收益来自于你在循环中做的微小改变。我愿意打赌,从展开循环中得到的任何改进都会太小而无法衡量,更不用说实际注意了(即使你将迭代次数从100增加到几百万)。 / p>

另一方面,如果你重写循环以消除每次迭代不必要的缓冲区刷新:

for ( int i = 1; i <= 100; i++ ) 
    cout << i << "\n";

[如果您没有意识到:std::endl将新行插入流刷新流。在大多数情况下(可能包括这个),缓冲区刷新是不必要的,可能是不可取的。删除它可以提高批次的速度 - 通过8:1或10:1的因子进行改进是相当普遍的。]

有可能根本无法衡量速度上的差异。有一个非常公平的机会,你可以在100次迭代中测量它,如果你尝试更多的迭代,差异很可能会变得非常明显。

当你处理一个不受I / O限制的循环,并且没有像这样明显的大规模改进时,循环展开很可能成为一个更有吸引力的选择。在这种情况下,您首先需要注意大多数编译器可以自动循环展开,因此尝试在源代码中执行此操作不太可能帮助很多,除非提供了机会其他优化(例如,如果你有一个循环,即使在迭代上真的做一件事,另一件在奇数迭代上,展开那两个迭代可以消除条件和跳跃等等,所以手工完成可能会提供一个有意义的改进,因为编译器可能不会“注意到”奇数/偶数模式并消除条件,跳转等。

另请注意,现代CPU可以(通常会)并行执行代码,并以推测方式执行代码,这可以消除循环的大部分开销。由于循环的分支几乎总是被占用(即,除了最后一次迭代之外的所有循环),CPU的分支预测器会将其预测为已采用,因此CPU可能同时“在飞行中”有几次迭代值的指令,即使你不要展开循环。循环本身的大多数代码(例如,递增i)可以与循环中的至少一些其他代码并行执行,因此无论如何循环的开销可能非常小。

编辑2:看看手头的具体问题,我认为我的工作方式有所不同。我没有将TTT板存储为2D阵列,而是将其存储为一对位图,一个用于X,另一个用于O.这使您可以在单个操作中测试整个获胜组合,而不是三个单独的比较。由于每行是3位,因此对于常量来说,最简单的方法是使用八进制:

static const std::array<short, 8> winners = {
    /* rows */      0007, 0070, 0700, 
    /* columns */   0111, 0222, 0444, 
    /* diagonals */ 0124, 0421
};

在这种情况下,我几乎肯定会使用循环:

char CheckForWinner(short X, short O) { 
    // `winners` definition from above goes here.

    for (int i=0; i<winners.size(); i++) {
        if (X & winners[i] == winners[i])
            return 'X';
        if (O & winners[i] == winners[i])
            return 'O';
    }
    return ' ';
}

这里最大的问题是你是否真的想要单独传递X和O板,或者传递两个短路阵列是否更有意义。使用阵列的明显优势是更容易进入对方板。例如,要测试是否允许在一个板中移动,您需要检查该位是否在另一个板中设置。将电路板存储在一个阵列中,您可以传递n表示您想要移动的电路板,并使用1-n来获取另一块电路板,在那里您将检查该位已经确定了。

答案 1 :(得分:4)

您所谈论的内容称为循环展开。性能权衡是复杂的,并且取决于编译器和执行环境的许多方面。有关问题的讨论,请参阅Wikipedia article on loop unwinding

答案 2 :(得分:3)

通过编码哪些位置是哪一行的一部分,您可以非常有效地执行赢取检查:

char square[3][3] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};
char player = 'x';
unsigned progress[2];

const unsigned lines[3][3] = {
    0x10010010,
    0x10001000,
    0x10000101,

    0x01010000,
    0x01001011,
    0x01000100,

    0x00110001,
    0x00101000,
    0x00100110
};

编码是“顶行,中行,底行,左列,中间列,右列,向下对角线,向上对角线”。

例如,左上角位置是顶行,左列和向下对角线的一部分。

只要您在同一行中有3个片段,该行已满并且您获胜,所以只需继续添加这些行,直到您达到3.您可以识别3个连续的2个1位,因此{{1}将为非零:

p & (p >> 1)

答案 3 :(得分:2)

在考虑循环展开时,有必要估计循环体与循环组织开销之间的权重比。

确实,即使是最简单的for循环也会增加几个指令开销。但在你的情况下,I / O调用的复杂性会使这些指令超重10-100次。

当循环体在内存中进行一些需要几个,可能是十几个asm指令的操作时,展开是有意义的。例如:

// Process digits starting fom the last one.
wchar_t carry_bit = 0;
while (curr_digit_offs >= 0)
{
    wchar_t ch = fpb[curr_digit_offs];
    fpb[curr_digit_offs--] = g_RawScan_MultiplyBy2[ch & 15] + carry_bit;
    carry_bit = (ch >= L'5') ? TRUE : FALSE;
}

在上面的例子中,循环体不调用任何外部函数。它只适用于内存中的数据结构。这意味着可以估计其复杂性。

在每种特殊情况下都需要单独估算。