【问题标题】:Optimising C code for small size - sharing static variables?为小尺寸优化 C 代码 - 共享静态变量?
【发布时间】:2021-08-19 22:55:42
【问题描述】:

我有两个函数,都和这个类似:

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 变量提升为全局变量(可变静态)。因为这两个函数是有效的原子操作(它是一个单线程 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%的FLASH内存!如果您使用 9600 波特,8N1 的串行端口连接到芯片,您可以输入字符并返回它们,并将位 0x20 翻转(大写到小写,反之亦然)。

当然,这不是一个严肃的项目,我只是在尝试看看我可以在这个芯片中塞进多少功能。我不会费心在汇编中重写它,我严重怀疑我能比 GCC 的优化器做得更好!

编辑

@Frank 询问了我正在使用的 IDE/编译器...

微芯片工作室 (7.0.2542)

传递给编译器的“所有选项”字符串avr-gcc...

-x c -funsigned-char -funsigned-bitfields -DDEBUG  -I"C:\Program Files (x86)\Atmel\Studio\7.0\Packs\atmel\ATtiny_DFP\1.8.332\include"  -Os -ffunction-sections -fdata-sections -fpack-struct -fshort-enums -g2 -Wall -mmcu=attiny5 -B "C:\Program Files (x86)\Atmel\Studio\7.0\Packs\atmel\ATtiny_DFP\1.8.332\gcc\dev\attiny5" -c -std=gnu99 -MD -MP -MF "$(@:%.o=%.d)" -MT"$(@:%.o=%.d)" -MT"$(@:%.o=%.o)" 

【问题讨论】:

  • 您可以更改函数的签名以获取初始化循环变量的参数。 void Bit_Delay(char count) { volatile char z = count; /* ... */ } 并称它为 Bit_Delay(12);Bit_Delay(18);
  • 我对某事感到困惑。您的 RAM 或闪存快用完了?您正在尝试组合两个单字节 RAM 变量来帮助一个快用完 FLASH(程序?)内存的程序?此外,写入的变量在堆栈上,这是预先分配的 RAM,而静态变量不在堆栈上——如果你让它工作,它可能需要更多的 RAM。
  • @Basya,RAM 不是问题(我有 32 个字节可以漫游),我只是想尝试一下我编写代码的方式如何影响最终的二进制大小(受 FLASH 约束)。我认为全局分配一次,然后简单地多次引用,这可能比拥有它的两个副本要小。我远不是低级编译器滑稽动作的专家,我正在学习:)
  • @Frank,我会在 Q 的底部添加一个编辑...

标签: c optimization microcontroller atmelstudio attiny


【解决方案1】:

我质疑以下假设:

这不起作用。很明显,编译器正在完全优化大部分与 z 相关的代码!然后代码无法正常运行(运行速度太快),编译后的二进制文件大小下降到大约 50% 左右。

查看https://gcc.godbolt.org/z/sKdz3h8oP,似乎实际上正在执行循环,但是,无论出于何种原因,当使用全局易失性z 时,每个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,但它似乎没有涵盖局部变量的情况。

【讨论】:

  • 哇!第二个代码示例是我所期望的 z++; 的样子(尽管减去负 1 对我来说有点特殊)。编译器的原始输出(一长串奇怪的减法)对我来说似乎完全奇怪。感谢您证明我的直觉是正确的(global volatile z),但我对实际结果的解释是错误的(我曾认为它完全失败了)。令人惊讶的是,将一个简单的字符提升为全局字符可以极大地改善代码大小。我感谢您的帮助。非常感谢。
  • @Wossname 这似乎是一个编译器错误:gcc.gnu.org/bugzilla/show_bug.cgi?id=33970
  • 讨厌。在这种情况下,无论如何,如果我将 z 提升到全局 volatile,我的代码中可能存在错误。我重新使用 z 作为计数器,然后在同一个循环中调用该函数。嗯,这就是为什么它在没有提升的情况下工作,并且在提升的情况下编译得很小,但如果我按照你的建议重新校准我的延迟循环,很可能会在实际操作中失败(我想这个循环可能会永远运行)。这并不重要,真的。我已经了解了 avr-gcc 的损坏程度。我将在 AVR 汇编中重新编写代码并完全删除“优化器”。
  • @Wossname 没有人强迫你使用 z 作为循环计数器:gcc.godbolt.org/z/erqr1WvM7
  • 我明白了。但是看看它在每个连续的z 增量之前和之后使用sts z,r20 之前使用lds r20,z 的疯狂方式。之间没有间隙,所以它会立即存储和加载它,当它完全没有必要时(除非有一些我不知道的晦涩的 AVR 弱点需要这样做)。不用担心。我对你的解释非常满意,我不会再去说服这个 C 代码了。
猜你喜欢
  • 2011-09-07
  • 2011-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多