8位MCU的C整数提升


14

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


1
我不这么认为,编译器会意识到目标变量是一个(无符号)字符,因此它不会费心计算前8位。仍然,我发现GCC有时在优化代码方面不是很好,因此,如果您在ASM中进行编码,则结果MGIHT会更快。但是,除非您要执行对时间要求严格的任务/中断,并且预算约束非常严格,否则您应该选择功能更强大的处理器并用C对其进行编程,或者不要担心性能较低(考虑时间)投放市场,更好的代码可读性/重用性,更少的错误,等等。)
next-hack

很抱歉没有时间检查。但是,我认为gcc有一个命令行标志,可以控制“整数提升”。甚至可能存在对特定代码段进行控制的编译指示。性能有多关键?在AVR的许多用途中,某些算法的速度差异不是问题。Foxus首先使代码正常工作。然后,如果存在性能问题,请找出问题所在。浪费时间在汇编器中编码很容易,只有您发现这没关系。
gbulmer

1
只需反汇编,看看编译器在做什么。从纯语言的角度来看,是的。这里的实现是非典型的。通常int会尝试将自身与寄存器大小对齐,如果您有16位寄存器,则8位算术实际上比16位便宜8位。但这是相反的,对于8位mcu,实现int很有意义作为16位。因此,您可能应该在关注uchar的地方使用uchar,但不要使它成为一种常见的编程习惯,因为这会在其他地方给您带来最大的伤害。
old_timer

3
切记:避免回答评论中的问题。
管道

4
此类问题最好向SO的C专家询问,因为这是一个纯软件问题。用C进行整数提升是一个比较复杂的话题-一般的C程序员会对此有很多误解。
伦丁

Answers:


16

长话短说:

始终将整数提升为16位-C标准强制执行此操作。但是,如果编译器可以推断出符号与提升类型时的符号相同,可以将计算优化到8位(嵌入式系统编译器通常非常擅长于此类优化)。

这并非总是如此!整数提升引起的隐式签名更改是嵌入式系统中错误的常见来源。

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


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等上)将节省您的内存,并且您想真正尝试保存它...如果您实际使用它,但是如此处所示,尽可能少由于要尽可能多地存储在内存中,因此要节省闪存是因为没有额外的变量,内存节省可能是真实的,也可能不是真实的。


2
“编译器过去常常不确定这样做的版本是什么,所以在我的职业生涯初期就遇到了这种情况,尽管编译器按乱序进行了推广(就像上面一样),尽管我告诉过它要进行uchar数学运算,不感到惊讶。” 这是因为嵌入式系统C编译器曾经具有可怕的标准一致性:)通常允许编译器进行优化,但是在这里不能推断出结果是否适合,unsigned char因此必须按要求将其提升为16位。按标准。
伦丁

1
@old_timer (a<<8)|b对于任何int16位系统总是错误的。a将隐式提升为int已签名。如果aMSB中保留一个值,则最终会将数据移入16位数字的符号位,这将导致未定义的行为。
伦丁

1
fun3很有趣..ny ...编译器未完全优化...考虑到r1在GCC中始终为0,并指示ra,rb,{rh,rl}变量a,b和结果的寄存器,编译器可以完成:1)mov rh,r1; 2)mov rl,ra; 2)添加rl,rb; 3)adc rh,rh; 4)退。4条指令,第7或8条...指令1可以在ldi rh,0中更改。
next-hack

1
如果指定使用的编译器和相关选项,这将是一个更好的答案。
罗素·博罗戈夫

1
最好避免使用int / char等,而应使用更显式和可读性的int16_t和int8_t。
用户

7

不一定,因为现代编译器在优化生成的代码方面做得很好。例如,如果您将z = x + y;所有变量都写在哪里unsigned char,则要求编译器unsigned int在执行计算之前将其提升为。但是,由于最终结果将完全相同而无需提升,编译器将生成仅添加8位变量的代码。

当然,情况并非总是如此,例如,结果z = (x + y)/2;将取决于高字节,因此将进行升级。仍然可以通过不将中间结果转换回来避免组装unsigned char

使用编译器选项可以避免某些此类效率低下的情况。例如,许多8位编译器都有实用程序或命令行开关来将枚举类型适合1个字节,而不是intC所要求的。


4
“要求编译器将它们提升为unsigned int”。否,编译器需要将它们提升为int,因为char很可能不会具有与int任何平台相同的转换等级。
伦丁

3
“例如,许多8位编译器都具有编译指示或命令行开关以将枚举类型适合1个字节,而不是C所要求的int。” C标准确实允许将枚举变量分配到1个字节中。它仅要求枚举常量必须为int(是的,这是不一致的)。C11 6.7.2.2Each enumerated type shall be compatible with char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined...
伦丁
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.