按位运算会导致意外的可变大小


24

语境

我们正在移植最初使用PIC单片机的8位C编译器编译的C代码。为了防止无符号全局变量(例如,错误计数器)回滚到零,使用了一个常见的习惯用法:

if(~counter) counter++;

此处的按位运算符将所有位取反,并且仅当counter小于最大值时该语句才为真。重要的是,无论变量大小如何,此方法均有效。

问题

我们现在的目标是使用GCC的32位ARM处理器。我们注意到,相同的代码会产生不同的结果。据我们所知,按位补码运算返回的值与我们期望的大小不同。为了重现这一点,我们在GCC中进行编译:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

在输出的第一行中,我们得到的是:i1个字节。但是,的按位补码i实际上是四个字节,这会引起问题,因为与此进行比较现在无法给出预期的结果。例如,如果这样做(在哪里i是正确初始化的uint8_t):

if(~i) i++;

我们将看到i从0xFF到0x00的“环绕”。与以前的编译器和8位PIC微控制器中预期的运行方式相比,GCC的行为有所不同。

我们知道我们可以通过如下方式解决此问题:

if((uint8_t)~i) i++;

或者,通过

if(i < 0xFF) i++;

但是,在这两种解决方法中,必须知道变量的大小,并且软件开发人员容易出错。这些上限检查在整个代码库中进行。有变量多种尺寸(例如,uint16_tunsigned char等),并在工作,不然代码库改变这些并不是我们期待的东西。

我们对问题的理解是否正确,是否有可用的解决方案,而无需在使用这种习语的每种情况下都需要重新访问?我们的假设正确吗,像按位补码之类的操作应返回与操作数相同大小的结果?似乎这将中断,具体取决于处理器体系结构。我觉得自己正在服用疯狂药丸,而C应该比这更便携。同样,我们对此的理解可能是错误的。

从表面上看,这似乎不是一个大问题,但是这种以前使用的习惯用法已在数百个地点使用,我们渴望在进行昂贵的更改之前先了解这一点。


注意:这里有一个看似相似但并非完全相同的问题:char的按位运算给出32位结果

我没有看到这里讨论的实际问题的症结,即按位补码的结果大小与传递给运算符的结果大小不同。


14
“我们的假设正确吗,像按位补码之类的操作应返回与操作数相同大小的结果?” 不,这是不正确的,整数促销适用。
Thomas Jager

2
虽然肯定相关,但我不认为这些是该特定问题的重复,因为它们没有提供解决问题的方法。
科迪·格雷

3
我觉得自己正在服用疯狂药丸,而C应该比这更便携。如果没有在8位类型上获得整数提升,则说明您的编译器不兼容C标准。在那种情况下,我认为您应该仔细检查所有计算,并在必要时进行修复。
user694733

1
我是唯一一个想知道除了真正不重要的计数器之外,什么逻辑可以使它“增加空间,否则就算了”的逻辑吗?如果要移植代码,可以使用int(4个字节)代替uint_8吗?那将在很多情况下避免您的问题。
冰球

1
@puck是的,我们可以将其更改为4个字节,但是与现有系统进行通信时会破坏兼容性。目的是要知道什么时候有任何错误,因此一个1字节的计数器本来就足够了,现在仍然如此。
Charlie Salts

Answers:


26

您所看到的是整数促销的结果。在大多数情况下,在表达式中使用整数值时,如果该值的类型小于int该值,则将其提升为intC标准的 6.3.1.1p2节对此进行了说明:

在可以使用int或 的表达式中unsigned int可以使用以下内容

  • 一个对象或表达一个整型(除intunsigned int),其整数转换秩小于或等于的秩intunsigned int
  • 一个类型为的位字段_Boolint ,signed int , orunsigned int`。

如果an int可以代表原始类型的所有值(受位字段的宽度限制),则该值将转换为an int。否则,它将转换为 unsigned int。这些称为整数促销。整数促销未更改所有其他类型。

因此,如果变量具有类型uint8_t和值255,则使用除强制类型转换或赋值之外的任何运算符都会先将其转换int为值255,然后再执行操作。这就是为什么sizeof(~i)给您4而不是1的原因。

6.5.3.3节描述了整数促销适用于~运营商:

~运算符的结果是其(提升的)操作数的按位补码(即,当且仅当未设置转换后的操作数中的相应位时,结果的每个位才被设置)。对操作数执行整数提升,并且结果具有提升的类型。如果提升的类型是无符号类型,则表达式~E等于该类型中可表示的最大值minus E

因此,假设为32位int,如果counter具有8位值,0xff则将其转换为32位值0x000000ff,并将其应用于~0xffffff00

处理此问题的最简单方法可能是,不必知道类型就是在递增后检查该值是否为0,是否递减该值。

if (!++counter) counter--;

无符号整数的回绕在两个方向上都有效,因此将值递减0将为您提供最大的正值。


1
if (!++counter) --counter;对于某些程序员而言,使用逗号运算符可能不那么奇怪。
埃里克·Postpischil

1
另一个选择是++counter; counter -= !counter;
Eric Postpischil

@EricPostpischil实际上,我更喜欢您的第一个选择。编辑。
dbush

15
无论您如何编写,这都是丑陋且难以理解的。如果您必须使用这样的习惯用法,请每个维护程序员帮忙,然后将其包装为内联函数increment_unsigned_without_wraparoundincrement_with_saturation。就个人而言,我将使用通用的三操作数clamp函数。
科迪·格雷

5
同样,您不能将此函数用作函数,因为对于不同的参数类型,它必须具有不同的行为。您必须使用类型通用宏
user2357112支持Monica

7

sizeof(i); 您要求变量i的大小,所以1

sizeof(〜i); 您要求表达式类型的大小,即int,在您的情况下为4


使用

如果(〜i)

要知道是否不值255(在您的情况下为uint8_t)不是很可读,只需执行

if (i != 255)

您将获得一个可移植且可读的代码


变量有多种大小(例如uint16_t和unsigned char等)

要管理任何大小的unsigned:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

该表达式是常量,因此在编译时进行计算。

#include <limits.h>表示CHAR_BIT#include <stdint.h>表示uintmax_t


3
该问题明确指出它们有多种尺寸要处理,因此!= 255是不够的。
埃里克·Postpischil

@EricPostpischil啊,是的,我忘记了,所以假设“ if(i!=((1u << sizeof(i)* 8)-1))”始终是未签名的?
bruno

1
对于unsigned对象,这将是不确定的,因为整个对象宽度的偏移不是由C标准定义的,但可以使用来固定(2u << sizeof(i)*CHAR_BIT-1) - 1
埃里克·波斯特皮希尔

哦,是的,ofc,CHAR_BIT,我的坏蛋
bruno

2
为了获得更广泛类型的安全,可以使用((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1
埃里克·Postpischil

5

x给定x一些无符号整数类型,以下是用于实现“加1 但限制在最大可表示值”的几个选项:

  1. 当且仅当x小于小于其类型可表示的最大值时,才添加一个:

    x += x < Maximum(x);

    有关的定义,请参见以下项目Maximum。该方法很有可能被编译器优化为有效的指令,例如比较,某种形式的条件集或移动以及加法。

  2. 比较该类型的最大值:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (这通过将2移位N -1位来计算2 N,其中N是in的位数。我们这样做不是取代1 N位,因为类型中位数的移位不是由C定义的。标准的。宏可能不熟悉一些;它是一个字节的位的数目,所以是在类型的比特数)。xCHAR_BITsizeof x * CHAR_BITx

    为了美观和清晰起见,可以将其包装在宏中:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
    
  3. 递增x并更正它是否归零,请使用if

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. x使用以下表达式递增并更正是否归零:

    ++x; x -= !x;

    这在名义上是无分支的(有时对性能有利),但是编译器可以与上述相同地实现,如果需要,可以使用分支,但如果目标体系结构具有合适的指令,则可以使用无条件指令。

  5. 使用上述宏的无分支选项是:

    x += 1 - x/Maximum(x);

    如果x是其类型的最大值,则结果为x += 1-1。否则为x += 1-0。但是,在许多体系结构上划分都有些慢。取决于编译器和目标体系结构,编译器可以将此优化为指令而不进行除法。


1
我只是无法鼓励自己推荐使用宏的答案。C具有内联函数。您没有在宏定义内做任何无法在内联函数内轻松完成的事情。而且,如果您要使用宏,请确保在策略上用括号括起来以便清楚:运算符<<的优先级非常低。Clang用发出警告-Wshift-op-parentheses。好消息是,优化的编译器不会在此处生成除法运算,因此您不必担心它运行缓慢。
科迪·格雷

1
@CodyGray,如果您认为可以使用一个函数执行此操作,请写一个答案。
Carsten S

2
@CodyGray:sizeof x无法在C函数内部实现,因为C x必须是具有某种固定类型的参数(或其他表达式)。它无法产生调用者使用的任何参数类型的大小。宏可以。
Eric Postpischil

2

在stdint.h之前,变量大小可能因编译器而异,并且C中的实际变量类型仍为int,long等,并且仍由编译器作者对其大小进行定义。没有一些标准,也没有针对特定的假设。然后,作者需要创建stdint.h来映射两个世界,这就是stdint.h的目的是将uint_this映射到int,long,short。

如果要从另一个编译器移植代码,并且它使用char,short,int,long,那么您必须仔细检查每种类型并自己进行移植,就无法解决它。要么您最终获得了正确的变量大小,但声明发生了变化,但是编写的代码有效。

if(~counter) counter++;

或...直接提供口罩或打字

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

归根结底,如果您希望此代码正常工作,则必须将其移植到新平台上。您的选择方式。是的,您必须花时间处理每种情况并正确处理,否则,您将继续返回此代码,这甚至更昂贵。

如果您在移植之前在代码上隔离了变量类型以及变量类型的大小,那么请隔离执行此操作的变量(应该很容易grep)并使用stdint.h定义更改其声明,希望将来不会更改,而且您会感到惊讶,但是有时会使用错误的标题,因此即使进行检查也可以使您晚上睡得更好

if(sizeof(uint_8)!=1) return(FAIL);

虽然这种编码方式有效(if(〜counter)counter ++;),但出于现在和将来的可移植性需求,最好使用掩码来专门限制大小(而不依赖于声明),当代码是首先编写的,或者只是完成了端口,然后您就不必再重新移植一天了。或者使代码更具可读性,然后执行x <0xFF then或x!= 0xFF之类的操作,则编译器可以将其优化为与上述任何一种解决方案相同的代码,只是使其更具可读性且风险更低...

取决于产品的重要性或要发送补丁/更新的次数,还是要开动卡车或步行去实验室修理几次,以决定是否要寻找快速解决方案或只是触摸受影响的代码行。如果只有一百个或几个不是那么大的端口。


0
6.5.3.3一元算术运算符
...
4 运算符的结果~为其(提升的)操作数的按位补码(即,当且仅当未设置转换后的操作数中的相应位时,结果的每个位才被设置)。对操作数执行整数提升,并且结果具有提升类型。如果提升的类型是无符号类型,则表达式~E等于该类型中可表示的最大值minus E

C 2011在线草案

问题在于,运算符在应用运算符之前~被提升为int

不幸的是,我认为没有简单的方法可以解决这个问题。写作

if ( counter + 1 ) counter++;

将无济于事,因为促销也同样适用。我唯一可以建议是创造最大的一些符号常量的值,你想要的对象来表示和测试针对:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;

我感谢整数提升的意义-看来这是我们遇到的问题。值得指出的是,在您的第二个代码示例中,-1不需要,因为这将导致计数器稳定在254(0xFE)。在任何情况下,如我的问题中所述,这种方法都不理想,因为参与此习惯用法的代码库中的变量大小不同。
Charlie Salts
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.