8 位 MCU 上的 C 整数提升

电器工程 微控制器 avr C
2022-01-07 05:05:00

以 avr-gcc 为例,int 类型被指定为 16 位宽。由于 C 中的整数提升,对 C 中的 8 位操作数执行操作会导致这些操作数转换为 16 位 int 类型。这是否意味着如果用 C 编写,AVR 上的所有 8 位算术运算将花费更长的时间如果由于 C 的整数提升而以汇编语言编写?

3个回答

长话短说:

整数提升到 16 位总是发生 - C 标准强制执行此操作。但是允许编译器将计算优化回 8 位(嵌入式系统编译器通常非常擅长这种优化),前提它可以推断出符号与提升类型时的符号相同。

这并非总是如此!由整数提升引起的隐式符号变化是嵌入式系统中常见的错误来源。

详细解释可以在这里找到:隐式类型提升规则

unsigned int fun1 ( unsigned int a, unsigned int b )
{
    return(a+b);
}

unsigned char fun2 ( unsigned int a, unsigned int b )
{
    return(a+b);
}

unsigned int fun3 ( unsigned char a, unsigned char b )
{
    return(a+b);
}

unsigned char fun4 ( unsigned char a, unsigned char b )
{
    return(a+b);
}

正如所料,fun1 都是整数,所以 16 位数学也是如此

00000000 <fun1>:
   0:   86 0f           add r24, r22
   2:   97 1f           adc r25, r23
   4:   08 95           ret

虽然技术上不正确,因为它是由代码调用的 16 位加法,但由于结果大小,即使未优化此编译器也会删除 adc。

00000006 <fun2>:
   6:   86 0f           add r24, r22
   8:   08 95           ret

促销发生在这里并不感到惊讶,编译器不习惯这样做,不确定是什么版本开始发生这种情况,在我职业生涯的早期就遇到了这种情况,尽管编译器促销乱序(就像上面一样),即使我也在做促销告诉它做 uchar 数学,并不感到惊讶。

0000000a <fun3>:
   a:   70 e0           ldi r23, 0x00   ; 0
   c:   26 2f           mov r18, r22
   e:   37 2f           mov r19, r23
  10:   28 0f           add r18, r24
  12:   31 1d           adc r19, r1
  14:   82 2f           mov r24, r18
  16:   93 2f           mov r25, r19
  18:   08 95           ret

理想情况下,我知道它是 8 位的,想要一个 8 位的结果,所以我只是告诉它一直做 8 位。

0000001a <fun4>:
  1a:   86 0f           add r24, r22
  1c:   08 95           ret

所以一般来说,最好以寄存器大小为目标,理想情况下是 (u)int 的大小,对于像这样的 8 位 mcu,编译器作者必须做出妥协……不要养成习惯使用 uchar 进行数学运算,您知道不需要超过 8 位,因为当您移动该代码或在具有更大寄存器的处理器上编写类似这样的新代码时,编译器必须开始屏蔽和符号扩展,有些在某些指令中本机执行,而其他人则没有。

00000000 <fun1>:
   0:   e0800001    add r0, r0, r1
   4:   e12fff1e    bx  lr

00000008 <fun2>:
   8:   e0800001    add r0, r0, r1
   c:   e20000ff    and r0, r0, #255    ; 0xff
  10:   e12fff1e    bx  lr

强制8位成本更高。我作弊了一点/很多,需要稍微复杂一些的例子才能以公平的方式看到更多。

根据评论讨论编辑

unsigned int fun ( unsigned char a, unsigned char b )
{
    unsigned int c;
    c = (a<<8)|b;
    return(c);
}

00000000 <fun>:
   0:   70 e0           ldi r23, 0x00   ; 0
   2:   26 2f           mov r18, r22
   4:   37 2f           mov r19, r23
   6:   38 2b           or  r19, r24
   8:   82 2f           mov r24, r18
   a:   93 2f           mov r25, r19
   c:   08 95           ret

00000000 <fun>:
   0:   e1810400    orr r0, r1, r0, lsl #8
   4:   e12fff1e    bx  lr

没有惊喜。虽然优化器为什么要留下那条额外的指令,但你不能在 r19 上使用 ldi 吗?(我问的时候就知道答案了)。

编辑2

为 avr

avr-gcc --version
avr-gcc (GCC) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

避免坏习惯与否 8 位比较

arm-none-eabi-gcc --version
arm-none-eabi-gcc (GCC) 7.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

显然优化只需要一秒钟就可以尝试使用您自己的编译器来查看它与我的输出相比如何,但无论如何:

whatever-gcc -O2 -c so.c -o so.o
whatever-objdump -D so.o

是的,将字节用于字节大小的变量,当然是在 avr、pic 等上,会节省你的内存,你想真正尝试保存它……如果你真的在使用它,但尽可能少地显示这里是将在内存中,尽可能多地在寄存器中,因此闪存节省来自没有额外的变量,内存节省可能是也可能不是真实的..

不一定,因为现代编译器在优化生成的代码方面做得很好。例如,如果您写z = x + y;where all variables are ,编译器需要在执行计算之前将unsigned char它们提升到。unsigned int但是,由于最终结果在没有提升的情况下完全相同,因此编译器将生成仅添加 8 位变量的代码。

当然,情况并非总是如此,例如结果z = (x + y)/2;将取决于高字节,因此会发生提升。仍然可以通过将中间结果转换回unsigned char.

使用编译器选项可以避免一些这样的低效率。例如,许多 8 位编译器具有编译指示或命令行开关,以将枚举类型放入 1 个字节中,而不是intC 所要求的。