【问题标题】:Why is this nested for loop so much slower than unrolling the same code?为什么这个嵌套的 for 循环比展开相同的代码要慢得多?
【发布时间】:2018-04-14 13:45:51
【问题描述】:

我正在使用 ATtiny85 和 128x64px OLED 构建一个小玩具控制台。在我最初的构建中,我使用了内置的 shiftOut()digitalWrite() 函数将显示数据转移到屏幕控制器。

这让我获得了大约 5fps,这有点令人失望。

我编写了自己的函数,该函数使用直接端口操作来发送数据,并且速度显着提高了 ~23fps,这还不错。这是那个函数:

void shift_out_block(block)
{
    byte b;
    for (byte i = 0; i < 8; i++)  
    {
        b = pgm_read_byte(block+i);

        for (byte j=0 ; j < 8 ; j++)
        {
            if ( !!( b & (1 << j)) )
            {
                PORTB |= 1 << SDA;
            }
            else
            {
                PORTB &= ~(1 << SDA);
            }

            PORTB |= 1 << SCL; // HIGH
            PORTB &= ~(1 << SCL); // LOW
        }
    }
}

23fps 还可以,但不是 30 甚至 60fps(如果是 24fps,我实际上会把它留在这里,但是奇数......)。

我明白为什么删除库调用和操作端口直接改善了很多事情 - 库被编写为在各种不同的 MCU 上工作。

我隐约记得循环解开是一个事情,所以我解开了内部for 循环:

void shift_out_block()
{
    byte b;
    for (byte i = 0; i < 8; i++)  
    {
        b = pgm_read_byte(block+i);

        if ( !!( b & (1 << 0)) )
        {
            PORTB |= 1 << SDA;
        }
        else
        {
            PORTB &= ~(1 << SDA);
        }

        PORTB |= 1 << SCL; // HIGH
        PORTB &= ~(1 << SCL); // LOW

        if ( !!( b & (1 << 1)) )
        {
            PORTB |= 1 << SDA;
        }
        else
        {
            PORTB &= ~(1 << SDA);
        }

        PORTB |= 1 << SCL; // HIGH
        PORTB &= ~(1 << SCL); // LOW

        if ( !!( b & (1 << 2)) )
        {
            PORTB |= 1 << SDA;
        }
        else
        {
            PORTB &= ~(1 << SDA);
        }

        PORTB |= 1 << SCL; // HIGH
        PORTB &= ~(1 << SCL); // LOW

        if ( !!( b & (1 << 3)) )
        {
            PORTB |= 1 << SDA;
        }
        else
        {
            PORTB &= ~(1 << SDA);
        }

        PORTB |= 1 << SCL; // HIGH
        PORTB &= ~(1 << SCL); // LOW

        if ( !!( b & (1 << 4)) )
        {
            PORTB |= 1 << SDA;
        }
        else
        {
            PORTB &= ~(1 << SDA);
        }

        PORTB |= 1 << SCL; // HIGH
        PORTB &= ~(1 << SCL); // LOW

        if ( !!( b & (1 << 5)) )
        {
            PORTB |= 1 << SDA;
        }
        else
        {
            PORTB &= ~(1 << SDA);
        }

        PORTB |= 1 << SCL; // HIGH
        PORTB &= ~(1 << SCL); // LOW

        if ( !!( b & (1 << 6)) )
        {
            PORTB |= 1 << SDA;
        }
        else
        {
            PORTB &= ~(1 << SDA);
        }

        PORTB |= 1 << SCL; // HIGH
        PORTB &= ~(1 << SCL); // LOW

        if ( !!( b & (1 << 7)) )
        {
            PORTB |= 1 << SDA;
        }
        else
        {
            PORTB &= ~(1 << SDA);
        }

        PORTB |= 1 << SCL; // HIGH
        PORTB &= ~(1 << SCL); // LOW
    }
}

完全不费力,复制粘贴 7 次。给了我将近 75fps - 原始函数在 ~42ms 内执行,新的 ugly 只需要 ~13ms。

出于兴趣,我将 send bit 部分拆分为一个单独的函数并调用了 8 次:

void shift_out_bit(bool bit)
{
    if ( bit )
    {
        PORTB |= 1 << SDA;
    }
    else
    {
        PORTB &= ~(1 << SDA);
    }

    PORTB |= 1 << SCL; // HIGH
    PORTB &= ~(1 << SCL); // LOW
}

void shift_out_block()
{
    byte b;
    for (byte i = 0; i < 8; i++)  
    {
        b = pgm_read_byte(block+i);

        shift_out_bit( !!( b & (1 << 0)) );
        shift_out_bit( !!( b & (1 << 1)) );
        shift_out_bit( !!( b & (1 << 2)) );
        shift_out_bit( !!( b & (1 << 3)) );
        shift_out_bit( !!( b & (1 << 4)) );
        shift_out_bit( !!( b & (1 << 5)) );
        shift_out_bit( !!( b & (1 << 6)) );
        shift_out_bit( !!( b & (1 << 7)) );
    }
}

~22ms 执行,或 45.4545454545 fps,这甚至不是一个好数字。

无论如何,我都不是 C 程序员 - Python 是我经常出没的地方(我最初确实在 Python/RPi 中启动了这个项目,但非常很快就放弃了!) .

为什么在这种情况下这样的核心语言功能会慢得多?随着我的项目变得越来越复杂,我还应该考虑哪些其他优化?

【问题讨论】:

  • 很抱歉,如果您知道这一点,但这看起来您有点在抨击 I2C。 ATtiny85 具有硬件 I2C。
  • 另一个可能没用的评论是您正在测试 if( !!( b & (1
  • void shift_out_block(block) 甚至不是一个有效的函数头。如果您不了解和理解该语言,就无法编写好的代码。
  • 我有点抨击 SPI ;) 和双重!!是从其他地方复制和粘贴的,我还没有弄清楚它实际上做了什么,看看我是否可以简化它!

标签: c avr


【解决方案1】:

考虑在最内层循环内完成的“有效负载”操作:

  • 检查b 中的特定位
  • 条件跳转处理PORTB |= 1 &lt;&lt; SDA vs.PORTB &amp;= ~(1 &lt;&lt; SDA)
  • PORTB 上的三个操作

这就是循环的展开版本所做的一切;不需要做任何其他事情,甚至不需要将1 向左移动j 次,因为编译器会计算常量表达式,而1 &lt;&lt; 4 会变成简单的16

另一方面,没有展开的循环必须做额外的事情来保持循环:

  • 递增j
  • j 与 8 进行比较
  • 1 向左移动j 位置
  • 无条件跳转到循环开头

当循环展开时,CPU 不再负担这些“非负载”指令,因此执行速度提高了。

为什么这种核心语言功能在这种情况下会慢很多?

许多现代编译器会根据优化设置自动为您展开循环。

【讨论】:

  • 这很有意义。我已经制作了许多“闪烁灯”级别的程序,这是我用 C 尝试过的最复杂的程序,但我不习惯接近金属;)谢谢
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-09-29
  • 2019-05-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-29
相关资源
最近更新 更多