如何优化C for循环?

时间:2009-07-30 07:46:32

标签: c optimization compiler-construction

我的代码中的瓶颈部分存在性能问题。基本上它是一个简单的嵌套循环。

分析问题表明程序花了很多时间只增加循环计数器(++)和终止测试(i / j< 8)。

观察汇编输出我看到两个计数器都没有得到寄存器,访问它们需要花费很多周期。使用“register”关键字并不能说服编译器将它们实际放入寄存器中。是否可以采取一些措施来优化计数器的访问时间?

这是装配输出。 C源只是一个带有i / j计数器的简单嵌套循环。

  2738  0.2479  2459  0.1707   :    1e6c:   jne    1dd1 <process_hooks+0x121>
  1041  0.0942  1120  0.0778   :    1e72:   addl   $0x1,0xffffffd4(%ebp)
  2130  0.1928  2102  0.1459   :    1e76:   cmpl   $0x8,0xffffffd4(%ebp)
  2654  0.2403  2337  0.1622   :    1e7a:   jne    1da0 <process_hooks+0xf0>
  809   0.0732   814  0.0565   :    1e80:   jmp    1ce2 <process_hooks+0x32>

根据要求,这里也是C代码。编译器是gcc btw:

for (byte_index=0; byte_index < MASK_SIZE / NBBY; byte_index++)
{
    if (check_byte(mask,byte_index))
    {
        for (bit_index=0; bit_index < NBBY; bit_index++)
        {
            condition_index = byte_index*NBBY + bit_index;
            if (check_bit(condition_mask,condition_index))
            {
                .
                .
                .
            }
        }
    }
}

由于

16 个答案:

答案 0 :(得分:13)

有两个可能的原因,它没有被放入登记册:

变量需要保存在内存中

如果您正在获取变量的地址或声明变量,则不会将其保存在寄存器中。它看起来不像你那样做,但它可能发生在...部分。

gcc在寄存器分配方面表现不佳。

这很有可能。 gcc似乎有一个糟糕的分配器(基于其开发人员的评论)。此外,注册分配是变化无常且难以推理的。您可能可以使用register allocator optimizations调整它以获得一些好处。如果您愿意,可以将其设置为that function only

gcc 4.4有一个新的寄存器分配器,应该更好,但也允许你选择分配算法。这将提供额外的调整内容。

你也可以尝试使用hot attribute来告诉gcc更加努力。

最后,您还可以使用gcc的--param标志来调整内容。它们暴露了内部编译器设置,所以这可能不应该轻易开始。

答案 1 :(得分:8)

在循环计数器中获得性能瓶颈时,您应该考虑unrolling循环。

编辑:与往常一样,在优化时,请确保您进行基准测试并说服自己获得所需的结果。

答案 2 :(得分:6)

使用intel编译器时得到的最佳结果(速度方面)。

你说的'register'关键字只是作为编译器的提示(就像内联一样)。

如果你真的认为这个循环是一个主要的瓶颈,那就输入原始组件。我知道它几乎不便携,但是再说一次,通常这并不重要,如果它应该是便携的......它只在一个特定的地方。

你甚至可以使用原始C代码#ifdef整个位以保持可移植性

答案 3 :(得分:6)

    for (bit_index=0; bit_index < NBBY; bit_index++)
    {
        condition_index = byte_index*NBBY + bit_index;
        if (check_bit(condition_mask,condition_index))
        {
            .
            .
            .
        }
    }

可以很容易;

    condition_index = byte_index * NBBY;
    for (bit_index=0; bit_index < NBBY; bit_index++, condition_index++)
    {
        if (check_bit(condition_mask,condition_index))
        {
            .
            .
            .
        }
    }

我喜欢将计算保持在正确的范围内。你在外部循环中拥有了这个的所有信息,但是选择在内部循环中进行。新循环稍微有点乱,但这可以避免,现在你的编译器更有可能正确地做事。 (它可能以前做过,但如果不检查组件,就无法确定。)

说到正确的范围,没有理由在循环之外声明循环计数器。这种C风格已经过时多年了,虽然它可能并不具有特定的性能劣势,但将事物限制在最小的逻辑范围内可以使代码更清晰,更易于维护。

对于8位,您可能会展开,但根据您的硬件,它可能无法正常工作。还有很多其他方法可以做到这一点,我可能错过了一些看过它。在硬件中,我在循环中使用条件通常对性能有害,但我没有看到任何明显的方法来避免它。我当然会考虑在外部循环中迭代位而不是字节,以避免在常见情况下的乘法。只是提出这个......我想在这种情况下不会有明显的优势。

答案 4 :(得分:5)

你需要确定这是一个瓶颈,在现代处理器上,指令被拆开,部分指令不按顺序执行,并且使用缓存和后备缓冲区,这完全有可能不会慢。

答案 5 :(得分:3)

This page表明“register关键字是一个有点过时的过程,因为很长一段时间以来,现代编译器中的优化器足够聪明,可以检测何时在寄存器上存储变量将是有利的,并且会在优化。那么,建议编译器在寄存器中存储变量只会使事情变得更慢,如果使用不正确“。

我猜这很大程度上取决于你的编译器和优化级别。正如其他人所说,这可能是-funroll-all-loops(gcc)的一个很好的候选者。

答案 6 :(得分:3)

这似乎是一个小问题,但不是使用表单:index ++,使用++ index;

基本原理是index ++需要在递增之前缓存当前rvalue,而++ index返回新计算的rvalue,它应该已经被缓存,从而保存了一个引用。

当然,一个好的编译器会优化它,所以它可能不是问题。

答案 7 :(得分:1)

我希望这两个函数是内联的(check_bit和check_byte),因为它们比任何寄存器变量都要慢得多。

如果编译器没有内联它们,请将它们内嵌到循环中。

答案 8 :(得分:1)

你应该改变你的设计,内环不应该存在于第一位 - 你应该避免使用位,将位检查转换为单字节检查。 我无法确切地告诉你如何,因为这是基于你所做的检查类型,但我假设涉及一个循环表shell。

修改 另外需要考虑的是,如果你真的想要更快地编写代码的一部分,你可能会使用特殊的CPU指令,你的编译器可能会对何时使用它们毫无头绪。 例如在英特尔上,有许多指令可以使用,直到SSE4等等,这是你可以比编译器更好地执行的地方,因为它无法知道你想要在算法级别实现什么。 有关详细信息,请查看英特尔(R)64和IA-32架构软件开发人员手册。 同样在这个级别,您可以从更好地控制管道中受益。

如果您不想编写程序集,有时会在指令中使用包装函数,以便在“C”中使用。

关于检查某位是否开启: 如果某个位打开,不确定你想要做什么,但是(假设你的位是字节对齐的):

假设您将在X上获得字节0110 0110。 你会想要做一些事情,也许打印按钮,如“比特1,2,5,6正在打开”。 你可以创建256个功能,每个功能都可以显示这种按摩。 你怎么知道激活哪一个? 函数编号shell正好是接收到的字节的值,因此您可以简单地使用 []运营商去那里。然而,它将是一个指向函数的指针表。 看起来应该是这样的:

//define the functions
void func0()
{
   printf("No Bits are on.");
}

void func1()
{
   printf("Bit 0 is on.");
}
.
.
.

//create the table
void  (*table[256])();
table[0] = &func0;
table[1] = &func1;
.
.
.

//the for loop
void  (*pointer_to_func)();
for...
{
   X = getByte();
   pointer_to_func = table[X]; //table shell contain 256 function pointers.
   pointer_to_func(); //call the function
}

这应该调用X位置的函数,并执行它,我假设位置X == 102(0110 0110的小数)的函数将是这样的:

printf(“比特1,2,5,6开启”);

参见The Function Pointer Tutorials ,特别是this

答案 9 :(得分:1)

您可以尝试将其重构为一个索引,看看是否会改变编译器的想法:

for (condition_index = 0; condition_index < MASK_SIZE;)
{
    if (check_byte(mask, condition_index / NBBY))
    {
        for (bound = condition_index + NBBY; condition_index < bound; condition_index++)
        {
            if (check_bit(condition_mask, condition_index))
            {
                /* stuff */
            }
        }
    }
    else
    {
        condition_index += NBBY;
    }
}

(希望NBBY是2的幂,因此除数将实现为转换)

答案 10 :(得分:1)

如果确实内部if()内的/ * stuff * /代码很少被执行(并且在它可能发生的次数或至少是合理的限制上存在先验已知的约束),改为双通解决方案可能会带来性能提升。这将消除嵌套循环中的寄存器压力。以下内容基于我以前的单指数答案:

for (n = 0, condition_index = 0; condition_index < MASK_SIZE;)
{
    if (check_byte(mask, condition_index / NBBY))
    {
        for (bound = condition_index + NBBY; condition_index < bound; condition_index++)
        {
            if (check_bit(condition_mask, condition_index))
            {
                condition_true[n++] = condition_index;
            }
        }
    }
    else
    {
        condition_index += NBBY;
    }
}

do {
    condition_index = condition_true[--n];
    /* Stuff */
} while (n > 0);

答案 11 :(得分:0)

您可以尝试展开循环。编译器可能会为您执行此操作,但如果没有,并且您真的需要性能,请自行完成。我假设您正在执行类似于每次迭代调用function(.., i, j, ..)的操作,因此只需将循环替换为:

function(.., 0, 0, ..)
...
function(.., 0, 7, ..)
function(.., 1, 0, ..)
...
function(.., 7, 7, ..)

有了更多的上下文(C源代码),可能会有更多有用的事情要做。坦率地说,如果2个堆栈分配的计数器(许多现代处理器具有特殊的加速器硬件来访问堆栈的最高位,几乎与寄存器一样快),我会感到震惊。在非玩具程序中引起明显的问题。

答案 12 :(得分:0)

如果编译器试图将计数器放入寄存器,则必须为循环内的每个函数调用保存和恢复寄存器(可能取决于定义这些函数的位置)。内联函数应该大大加快速度(如果这确实是你的瓶颈)。

答案 13 :(得分:0)

假设分析信息是正确的,并且确实是导致瓶颈的增量操作,您可能会滥用一些无序执行:

for (byte_index = 0; byte_index < MASK_SIZE / NBBY; )
{
    if (check_byte(mask,byte_index++))
    {
        condition_index = byte_index*NBBY;
        for (bit_index=0; bit_index < NBBY; )
        {
            if (check_bit(condition_mask,condition_index + bit_index++))
            {
                ...
            }
        }
    }
}

(上述代码段不会出于显而易见的原因,但您应该明白这一点:)

另外,从C片段中的函数/宏名称开始,我假设您正在使用位掩码来执行操作。有助于我提高性能的一件事是迭代掩码数组而不是对输入进行动态计算,例如

for (byte_index = 0; byte_index < MASK_SIZE / NBBY; byte_index++)
{
    if (check_byte(mask,byte_index))
    {
        const char masks[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };
        for (mask_index=0; mask_index < sizeof(masks) / sizeof(masks[0]); mask_index++)
        {
            if (check_bit(masks[mask_index], byte_index))
            {
                ...
            }
        }
    }
}

...编译器可能有更好的机会正确优化/展开。

答案 14 :(得分:0)

没有看到内循环中的内容,尝试优化循环是没有意义的。看起来代码是为x86 32位生成的。如果循环中的计算需要多个寄存器,则编译器无需将循环计数器保存在寄存器中,因为它必须将它们溢出到堆栈中。然后,根据内部循环中使用的指令,寄存器分配可能存在一些问题。移位仅使用ECX寄存器,因为计数,乘法和除法对所使用的寄存器有限制,字符串命令使用ESI和EDI作为寄存器,减少了编译器保存值的机会。 正如其他人已经说过的那样,循环中间的调用也无济于事,因为无论如何都必须保存寄存器。

答案 15 :(得分:0)

我会尝试以不同的方式看待问题。

代码究竟在做什么,也许在解释它的作用时,可以使用更有效的不同算法?

例如,我经常看到通过将列表分成两个链接列表,一个是“活动”项目和一个“非列表,可以更快地对大项目列表进行迭代的代码 - 有效的“项目,然后有代码将项目从一个列表移动到另一个列表,因为项目已分配和免费。我认为这会给你最好的结果。