为小尺寸优化 C 代码 - 共享静态变量?
Optimising C code for small size - sharing static variables?
我有两个函数,都和这个类似:
void Bit_Delay()
{
//this is a tuned tight loop for 8 MHz to generate timings for 9600 baud
volatile char z = 12;
while(z)
{
z++;
z++;
z++;
z++;
z -= 5;
}
}
(第二个函数是类似的,它使用 18 而不是 12 作为计数器)。
代码按原样完美运行(z 在内部出现在每个函数的本地),但我试图在我达到(非常)有限的可用闪存之前将更多功能塞入我的可执行文件中。
我的想法是将 z
变量提升为全局变量(volatile static)。因为这两个函数实际上是原子操作(它是单线程 CPU 并且没有中断在起作用),我认为这两个函数可以共享单个变量,从而节省一点堆栈操作.
这没有用。很明显,编译器正在完全优化与 z
相关的大部分代码!然后代码无法正常运行(运行 太快了),编译后的二进制文件的大小下降到大约 50% 左右。
我意识到我需要将 z 变量标记为 volatile 以防止编译器删除它知道每次都在计算固定(因此可简化为常量)数字的代码。
问题:
我可以进一步优化它,并诱使编译器保持两个函数的完整性吗?我正在编译“-Os”(针对小型二进制文件进行优化)。
这是为那些在家一起玩的人准备的完整节目...
#include <avr/io.h>
#define RX_PIN (1 << PORTB0) //physical pin 3
#define TX_PIN (1 << PORTB1) //physical pin 1
void Bit_Delay()
{
//this is a tuned tight loop for 8 MHz to generate timings for 9600 baud
volatile char z = 12;
while(z)
{
z++;
z++;
z++;
z++;
z -= 5;
}
}
void Serial_TX_Char(char c)
{
char i;
//start bit
PORTB &= ~TX_PIN;
Bit_Delay();
for(i = 0 ; i < 8 ; i++)
{
//output the data bits, LSB first
if(c & 0x01)
PORTB |= TX_PIN;
else
PORTB &= ~TX_PIN;
c >>= 1;
Bit_Delay();
}
//stop bit
PORTB |= TX_PIN;
Bit_Delay();
}
char Serial_RX_Char()
{
char retval = 0;
volatile char z = 18; //1.5 bits delay
//wait for idle high
while((PINB & RX_PIN) == 0)
{}
//wait for start bit falling-edge
while((PINB & RX_PIN) != 0)
{}
//1.5 bits delay
while(z)
{
z++;
z++;
z++;
z++;
z -= 5;
}
for(z = 0 ; z < 8 ; z++)
{
retval >>= 1; //make space for the new bit
retval |= (PINB & RX_PIN) << (8 - RX_PIN); //get the bit and store it
Bit_Delay();
}
return retval;
}
int main(void)
{
CCP = 0xd8; //protection signature for clock registers (see datasheet)
CLKPSR = 0x00; //set the clock prescaler to "div by 1"
DDRB |= TX_PIN;
PORTB |= TX_PIN; //idle high
while (1)
Serial_TX_Char(Serial_RX_Char() ^ 0x20);
}
目标 CPU 是一个 Atmel ATTiny5
微控制器,上面的代码使用了 94.1% 的闪存!如果您使用串行端口以 9600 波特、8N1 连接到芯片,您可以输入字符,然后 returns 它们的位 0x20 翻转(大写到小写,反之亦然)。
当然,这不是一个严肃的项目,我只是想看看我可以将多少功能塞进这个芯片。我不会费心在汇编中重写它,我严重怀疑我能比 GCC 的优化器做得更好!
编辑
@Frank 询问了我正在使用的 IDE / 编译器...
Microchip Studio (7.0.2542)
传递给编译器的“所有选项”字符串avr-gcc
...
-x c -funsigned-char -funsigned-bitfields -DDEBUG -I"C:\Program Files (x86)\Atmel\Studio.0\Packs\atmel\ATtiny_DFP.8.332\include" -Os -ffunction-sections -fdata-sections -fpack-struct -fshort-enums -g2 -Wall -mmcu=attiny5 -B "C:\Program Files (x86)\Atmel\Studio.0\Packs\atmel\ATtiny_DFP.8.332\gcc\dev\attiny5" -c -std=gnu99 -MD -MP -MF "$(@:%.o=%.d)" -MT"$(@:%.o=%.d)" -MT"$(@:%.o=%.o)"
我质疑以下假设:
This didn't work. It is clear that the compiler is optimising-out much of the code related to z completely! The code then fails to function properly (running far too fast), and the size of the compiled binary drops to about 50% or so.
查看 https://gcc.godbolt.org/z/sKdz3h8oP,似乎实际上正在执行循环,但是,无论出于何种原因,每个 z++
,当使用全局 volatile z
时,来自:
subi r28,lo8(-(1))
sbci r29,hi8(-(1))
ld r20,Y
subi r28,lo8((1))
sbci r29,hi8((1))
subi r20,lo8(-(1))
subi r28,lo8(-(1))
sbci r29,hi8(-(1))
st Y,r20
subi r28,lo8((1))
sbci r29,hi8((1))
至:
lds r20,z
subi r20,lo8(-(1))
sts z,r20
您需要重新校准 12、18 和 5 常量以使波特率正确(因为每个循环中执行的指令较少),但编译版本中存在逻辑。
需要说明的是:这对我来说真的很奇怪,本地 volatile 版本显然没有被正确编译。我确实在这些方面发现了一个旧的 gcc 错误:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=33970,但它似乎没有涵盖局部变量的情况。
我有两个函数,都和这个类似:
void Bit_Delay()
{
//this is a tuned tight loop for 8 MHz to generate timings for 9600 baud
volatile char z = 12;
while(z)
{
z++;
z++;
z++;
z++;
z -= 5;
}
}
(第二个函数是类似的,它使用 18 而不是 12 作为计数器)。
代码按原样完美运行(z 在内部出现在每个函数的本地),但我试图在我达到(非常)有限的可用闪存之前将更多功能塞入我的可执行文件中。
我的想法是将 z
变量提升为全局变量(volatile static)。因为这两个函数实际上是原子操作(它是单线程 CPU 并且没有中断在起作用),我认为这两个函数可以共享单个变量,从而节省一点堆栈操作.
这没有用。很明显,编译器正在完全优化与 z
相关的大部分代码!然后代码无法正常运行(运行 太快了),编译后的二进制文件的大小下降到大约 50% 左右。
我意识到我需要将 z 变量标记为 volatile 以防止编译器删除它知道每次都在计算固定(因此可简化为常量)数字的代码。
问题:
我可以进一步优化它,并诱使编译器保持两个函数的完整性吗?我正在编译“-Os”(针对小型二进制文件进行优化)。
这是为那些在家一起玩的人准备的完整节目...
#include <avr/io.h>
#define RX_PIN (1 << PORTB0) //physical pin 3
#define TX_PIN (1 << PORTB1) //physical pin 1
void Bit_Delay()
{
//this is a tuned tight loop for 8 MHz to generate timings for 9600 baud
volatile char z = 12;
while(z)
{
z++;
z++;
z++;
z++;
z -= 5;
}
}
void Serial_TX_Char(char c)
{
char i;
//start bit
PORTB &= ~TX_PIN;
Bit_Delay();
for(i = 0 ; i < 8 ; i++)
{
//output the data bits, LSB first
if(c & 0x01)
PORTB |= TX_PIN;
else
PORTB &= ~TX_PIN;
c >>= 1;
Bit_Delay();
}
//stop bit
PORTB |= TX_PIN;
Bit_Delay();
}
char Serial_RX_Char()
{
char retval = 0;
volatile char z = 18; //1.5 bits delay
//wait for idle high
while((PINB & RX_PIN) == 0)
{}
//wait for start bit falling-edge
while((PINB & RX_PIN) != 0)
{}
//1.5 bits delay
while(z)
{
z++;
z++;
z++;
z++;
z -= 5;
}
for(z = 0 ; z < 8 ; z++)
{
retval >>= 1; //make space for the new bit
retval |= (PINB & RX_PIN) << (8 - RX_PIN); //get the bit and store it
Bit_Delay();
}
return retval;
}
int main(void)
{
CCP = 0xd8; //protection signature for clock registers (see datasheet)
CLKPSR = 0x00; //set the clock prescaler to "div by 1"
DDRB |= TX_PIN;
PORTB |= TX_PIN; //idle high
while (1)
Serial_TX_Char(Serial_RX_Char() ^ 0x20);
}
目标 CPU 是一个 Atmel ATTiny5
微控制器,上面的代码使用了 94.1% 的闪存!如果您使用串行端口以 9600 波特、8N1 连接到芯片,您可以输入字符,然后 returns 它们的位 0x20 翻转(大写到小写,反之亦然)。
当然,这不是一个严肃的项目,我只是想看看我可以将多少功能塞进这个芯片。我不会费心在汇编中重写它,我严重怀疑我能比 GCC 的优化器做得更好!
编辑
@Frank 询问了我正在使用的 IDE / 编译器...
Microchip Studio (7.0.2542)
传递给编译器的“所有选项”字符串avr-gcc
...
-x c -funsigned-char -funsigned-bitfields -DDEBUG -I"C:\Program Files (x86)\Atmel\Studio.0\Packs\atmel\ATtiny_DFP.8.332\include" -Os -ffunction-sections -fdata-sections -fpack-struct -fshort-enums -g2 -Wall -mmcu=attiny5 -B "C:\Program Files (x86)\Atmel\Studio.0\Packs\atmel\ATtiny_DFP.8.332\gcc\dev\attiny5" -c -std=gnu99 -MD -MP -MF "$(@:%.o=%.d)" -MT"$(@:%.o=%.d)" -MT"$(@:%.o=%.o)"
我质疑以下假设:
This didn't work. It is clear that the compiler is optimising-out much of the code related to z completely! The code then fails to function properly (running far too fast), and the size of the compiled binary drops to about 50% or so.
查看 https://gcc.godbolt.org/z/sKdz3h8oP,似乎实际上正在执行循环,但是,无论出于何种原因,每个 z++
,当使用全局 volatile z
时,来自:
subi r28,lo8(-(1))
sbci r29,hi8(-(1))
ld r20,Y
subi r28,lo8((1))
sbci r29,hi8((1))
subi r20,lo8(-(1))
subi r28,lo8(-(1))
sbci r29,hi8(-(1))
st Y,r20
subi r28,lo8((1))
sbci r29,hi8((1))
至:
lds r20,z
subi r20,lo8(-(1))
sts z,r20
您需要重新校准 12、18 和 5 常量以使波特率正确(因为每个循环中执行的指令较少),但编译版本中存在逻辑。
需要说明的是:这对我来说真的很奇怪,本地 volatile 版本显然没有被正确编译。我确实在这些方面发现了一个旧的 gcc 错误:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=33970,但它似乎没有涵盖局部变量的情况。