为什么这个嵌套的 for 循环比展开相同的代码慢得多?
Why is this nested for loop so much slower than unrolling the same code?
我正在使用 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.
出于兴趣,我将 发送位 部分分解为一个单独的函数并调用了 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)) );
}
}
~22 毫秒执行,或 45.4545454545 fps,这甚至不是一个很好的数字。
无论如何我都不是 C 程序员 - Python 是我经常出没的地方(我最初是在 Python/RPi 开始这个项目的,但是 非常 很快就放弃了!)。
为什么这样一个核心语言特性在这种情况下会慢很多?随着我的项目变得越来越复杂,我还应该考虑哪些其他优化?
考虑在最内层循环中完成的 "payload" 操作:
- 检查
b
中的特定位
- 条件跳转处理
PORTB |= 1 << SDA
vs. PORTB &= ~(1 << SDA)
- 对
PORTB
的三个操作
这就是循环的展开版本所做的一切;不需要做任何其他事情,甚至不需要将 1
向左移动 j
次,因为编译器计算常量表达式,并且 1 << 4
简单地变为 16
.
另一方面,没有展开的循环必须做额外的事情来保持循环:
- 递增
j
- 比较
j
和 8
- 向左
1
移动 j
个位置
- 无条件跳转到循环开头
当循环展开时,CPU 不再受这些 "non-payload" 指令的负担,因此执行速度提高了。
Why is such a core language feature so much slower in this situation?
许多现代编译器会根据优化设置自动为您展开循环。
我正在使用 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.
出于兴趣,我将 发送位 部分分解为一个单独的函数并调用了 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)) );
}
}
~22 毫秒执行,或 45.4545454545 fps,这甚至不是一个很好的数字。
无论如何我都不是 C 程序员 - Python 是我经常出没的地方(我最初是在 Python/RPi 开始这个项目的,但是 非常 很快就放弃了!)。
为什么这样一个核心语言特性在这种情况下会慢很多?随着我的项目变得越来越复杂,我还应该考虑哪些其他优化?
考虑在最内层循环中完成的 "payload" 操作:
- 检查
b
中的特定位
- 条件跳转处理
PORTB |= 1 << SDA
vs.PORTB &= ~(1 << SDA)
- 对
PORTB
的三个操作
这就是循环的展开版本所做的一切;不需要做任何其他事情,甚至不需要将 1
向左移动 j
次,因为编译器计算常量表达式,并且 1 << 4
简单地变为 16
.
另一方面,没有展开的循环必须做额外的事情来保持循环:
- 递增
j
- 比较
j
和 8 - 向左
1
移动j
个位置 - 无条件跳转到循环开头
当循环展开时,CPU 不再受这些 "non-payload" 指令的负担,因此执行速度提高了。
Why is such a core language feature so much slower in this situation?
许多现代编译器会根据优化设置自动为您展开循环。