查看此C代码:
1 2 3
| byte x = 1;
byte y = 2;
byte z = x + y; // ERROR: Cannot implicitly convert type 'int' to 'byte' |
对byte类型(或short类型)执行的任何数学运算的结果都会隐式转换回整数。解决方案是显式地将结果强制转换回一个字节:
1
| byte z = (byte)(x + y); // this works |
我想知道的是为什么?它是建筑吗?Philosophical?
我们有:
- int+int=int。
- long+long=long。
- float+float=float。
- double+double=double。
为什么不:
- byte+byte=byte。
- short+short=short?
一点背景知识:我正在对"小数字"(即<8)执行一长串计算,并将中间结果存储在一个大数组中。使用字节数组(而不是int数组)更快(因为缓存命中)。但是,广泛的字节转换在代码中传播,使得代码更不可读。
- stackoverflow.com/questions/927391/&hellip;
- 我想这和用几个加法溢出一个字节有多容易有关。但是,我认为这应该留给程序员,而不是像这样在架构上限制它。
- 你确定你没有进行微优化吗?IIRC,在32位机器上,字节将在32位边界处对齐以优化访问,即一个字节实际上使用内存中的4个字节。
- 埃里克·利珀特,我们需要你的时候你在哪里??对C标准的广博知识,拯救!
- 这不是埃里克对标准的了解,而是他对语言设计的了解,而不是为什么。但是的,埃里克的回答是非常明确的:)
- 下面的各种设想是设计考虑的合理近似值。更一般地说:我不认为字节是"数字";我认为它们是可以解释为数字、字符、颜色等的位模式。如果要对它们进行数学运算并将它们视为数字,那么将结果移动到更常见的数据类型(即数字)中是有意义的。
- @埃里克:这对字节来说很有意义,但对short/ushort来说可能没有那么有意义。
- 埃里克-你的评论真的是对这个问题最好的回答。想为它创建一个答案吗?
- 迈克尔说的。把它作为答案,我会投反对票。
- 如果您的数字总是小于8,那么您可以按字节存储其中的两个,并再次将缓存未命中减半。
- @埃里克:byte1 | byte2根本不把它们当作数字。这就是将它们精确地视为比特模式。我理解你的观点,但每次我对c中的字节进行算术时,我都会把它们当作位而不是数字,而这种行为总是在这种情况下发生的。
- @尽管字节看起来只有8位,但当用作局部变量时,C会在堆栈上的4或8字节边界上对它们进行舍入。寄存器的用法完全相同——尽管86上的regs是32位的,但从msil生成的asm不会将4字节的变量打包到一个寄存器中。将有很多或加载和保存,就像32位值一样。
- 整数和蓝色的可能重复,短+=短问题
- @GSERG这个问题是第一个,在整个董事会中有更多的选票。你的关门方向不对。
- @罗伯特卡塔诺是的,这个问题比较古老,但是另一个问题包含了埃里克·利珀特的一个更有价值的答案。我会指定它作为规范的答案。
- @那么,GSERG的问题应该被合并,但这是一个非常危险的断言,通过"谁"来回答问题,而不是审查内容来判断问题。埃里克·利珀特也在这里评论了这个问题,我有一个乔恩·斯基特。我的一对打败了你的一对,不确定我们应该玩那个游戏。加斯特,我写过这个特定问题的优点(几百次),结果发现它被社区关闭了,这看起来很有趣。经典。
- @罗伯特卡塔诺然后进行最后的近距离投票,重新打开并留下一个链接到另一个。除了一个问题,我不是在结束所有的问题之后,我只是想把它们联系起来。
- 这是我在别处写的一个答案,它包含一个程序来识别这个编译器驱动的自动升级到EDCOX1(1)的过程(C或C++至少):STACKOFFROUT.COM/A/43578929/4618897。
- @Ericlippert(是的,我知道是几年后的事了),这并不能解释为什么在byte上执行逻辑操作时存在完全相同的问题。
代码段的第三行:
实际上意味着
1
| byte z = (int) x + (int) y; |
所以,字节上没有+运算,字节首先被转换为整数,两个整数相加的结果是一个(32位)整数。
- 我尝试过下面的代码,但它仍然不起作用。字节Z=(字节)x+(字节)y;
- 这是因为对于字节没有+操作(见上文)。尝试字节Z=(字节)((int)x+(int)y)
- 这必须是最正确、最简洁的答案。字节之间没有要添加的操作数,因此,这清楚地显示了为什么结果是int,而不是解释为什么"添加两个字节"有效(从未发生过),因为发生的唯一事情是添加2 int。
- 我看了所有其他答案都头晕了(乔恩·斯基特先生没有冒犯)。我发现这是最简单的答案,它正确地描述了引擎盖下面发生的事情。谢谢!
- 下面是我在其他地方写的一个答案,其中包含一个程序,用于确定此编译器驱动的对int的自动升级何时发生:stackoverflow.com/a/43578929/4561877
就"为什么会发生这种情况"而言,这是因为没有任何由c定义的用于字节、s byte、short或ushort算术的运算符,正如其他人所说。这个答案是关于为什么没有定义这些操作符。
我相信这基本上是为了表现。处理器具有本地操作,可以非常快地用32位进行算术。自动将结果转换回一个字节是可以做到的,但如果您实际上不希望这种行为发生,则会导致性能损失。
我认为这是在一个带注释的C标准中提到的。看。。。
编辑:令人恼火的是,我现在已经浏览了注释的ecma c_2规范、注释的ms c_3规范和注释cli规范,据我所见,它们都没有提到这一点。我确信我已经看到了上面给出的原因,但如果我知道在哪里,我会被激怒的。抱歉,推荐粉丝:(
- 很抱歉这么说,但我觉得这不是最好的答案。
- 你对每一个你认为不是最好的答案都投了反对票吗?;)
- (只是为了澄清一下,我不是真的要找你麻烦。似乎每个人都有自己的投票否决标准,这没关系。如果我认为答案是积极有害的,而不仅仅是不理想的,我只会投反对票。)
- 我用投票作为一种手段来获得对高层的"最佳"回答。事实上,我发现你在回答中根本没有说什么,这是我投反对票的主要原因。另一个原因可能是我主观的感觉,你的代表在投票时会给你一个很大的奖金,而你会得到更好的答案。
- 在我看来,最好的办法就是向上投赞成票。老实说,我认为这里最能说明问题的答案是埃里克的评论…但除此之外,对于设计透视图(与"编译器正在做什么"透视图相反),我认为除了"性能"之外没有太多答案。特别是,我不赞成"它可以防止溢出"的论点(17票),因为这意味着in t+in t=long。
- 如果你指的是迈克尔·彼得罗塔的回答:主要论点是我的雷蒙德·陈的博客文章和他优秀的"假设"例子。IMO没有人会建议32+240必须是16,因为这些数字是以字节存储的。回到你的第二条评论:你是对的。既然你的回答一点都不坏也不错,我没有理由投反对票。
- 我认为,对于对称性,32+240变为16和int.maxvalue+1变为int.minvalue一样合乎逻辑(modulo eric关于字节的评论实际上不是一个数字,而是一个比特集合)。很高兴我们在C中有一个检查上下文的概念。
- 有些人似乎不喜欢这个"无聊"的答案,因为它太实际了。他们想要更具概念的东西。对我来说,这个实际的答案似乎更为合理:当你设计一个规范时,你也需要考虑到实际的考虑。一个int被设计成使用一个CPU来添加,一个字节被设计成存储数据。进行添加时,将使用为添加而优化的数据类型。
- @投反对票的人:有什么理由吗?
- @乔恩不仅做到了这一点(可能),他还把这里的每一条评论都标记了出来。
- @乔斯基特:顺便问一句,你有注释C规格的链接吗?或者你有硬拷贝?
- @威尔:我有硬拷贝-很值得一看:amazon.com/dp/0321741765
- @琼斯基特:谢谢。
- 我不明白为什么有些人会反对这个答案;因为首先要指出的核心是"为什么那些操作员没有定义",这比简单地说"他们没有定义"更有意义。
- 回到主题…我猜想这是因为一个字节更常用于精确地表示8位信息,而int(和long等)则更常用于表示一个整数。int通常大于其各部分的总和,而byte通常用于位字段、原始内存访问等。如果将byte用作数字(例如通过添加另一个数字),则可以推断它是更常见的数字类型。
- @Jonskeet是我见过的唯一一个类似于你所说的"简而言之"的地方:8位和16位积分缺少自己的算术运算符,编译器根据需要将它们隐式转换为更大的类型(Int32)。
- 隐马尔可夫模型。。。大多数32位处理器架构本身不支持8位和16位算术运算吗?我很肯定x86是这样的,因为我在x86程序集中做过很多次。此外,目前大多数x86芯片实际上是64位的,因此,根据这个答案的论点,所有类型的芯片在进行数学运算之前都应该自动扩展到64位,而事实并非如此。
- @我真的不知道。我肯定我在某个有说服力的地方读过这个效率论点,但我现在记不起在哪里:(
- @琼斯基特:你描述的原因正是ARM架构的原因。基于ARMv4的处理器可以有效地加载和存储8、16和32位数据。然而,大多数ARM数据处理操作仅为32位。因此,尽可能对局部变量使用32位数据类型int或long。避免使用char和short作为局部变量类型,即使您正在操作第107页的8位或16位值链接。
我以为我以前在某个地方见过这个。从这篇文章,旧的新东西:
Suppose we lived in a fantasy world
where operations on 'byte' resulted in
'byte'.
1 2 3
| byte b = 32;
byte c = 240;
int i = b + c; // what is i? |
In this fantasy world, the value of i
would be 16! Why? Because the two
operands to the + operator are both
bytes, so the sum"b+c" is computed as
a byte, which results in 16 due to
integer overflow. (And, as I noted
earlier, integer overflow is the new
security attack vector.)
编辑:雷蒙德是防守,本质上,C和C++是最初采取的方法。在评论中,他辩护了这样一个事实:基于语言向后兼容性,C采取了同样的方法。
- 对于整数,如果我们将其相加并溢出,它不会自动将其转换为不同的数据类型,但是为什么要使用字节呢?
- @Ryan:我想微软在字节算术上预见到的问题比在int数学上预见到的要多,因为字节的动态范围较小。
- 有了ints,它就会溢出。尝试添加int.maxvalue+1得到-2147483648而不是2147483648。
- @朗霍恩213:是的,这就是瑞安说的:int-math可以溢出,但int-math不会返回long。
- 确切地。如果这是一种安全措施,那么它是一种执行得非常差的措施;)
- 我不确定雷蒙德(这一次)是否应该被视为这方面的权威。阅读博客上的评论。他在C语言中对此的辩护基本上是"因为C++这样做"博客。MSDN.COM/OLDNeWest/档案/ 2004 / 03 / 10/87247。
- 我会说这只是懒惰。请看下面我的答案。所有的开发人员都很懒惰,他们只是不想让操作员再次过载。
- @Ryan:"懒惰"是对C语言设计师的一个相当大的指控,因为这是一个基本的数学问题。如果你想指控他们什么,就把它变成"与C/C++的过度向后兼容"。
- Java也做同样的事情。不管想法的来源是什么,它似乎与语言设计者有着相当深的渊源。
- "与C/C++的过度向后兼容"可能是一个更好的方法。但是我们可以把问题扩展到为什么C是这样设计的,然后我们重新开始。
- C被设计成在非常原始的处理器(按照我们的标准)上高效运行,这些处理器除了标准的字大小外,可能没有关于整数算术的指令。如果您查看"C编程语言",它提到"int"通常是编译器目标处理器的"本机"字大小。
- justjeff:我能说出更多的语言,比我能说出更多的语言,这些语言可以无声地提升到bignums。对于语言设计人员来说,它似乎没有C类语法那么"根深蒂固"。
- vb.net的工作方式与幻想世界相似。我不会说那是好事。
- @Jonskeet可能不是一个安全/安全问题,但事实上,在处理器级别,短+短溢出是不可检测的。也就是说,如果实际的处理器指令.NET将操作映射到32位,并且它是checked算术,那么处理器将不会标记溢出,因为afaitk它不是32位溢出,即使它简而言之是溢出。因此,除了您在回答中指出的性能问题外,检查的算术将受到更大的影响,因为.NET不能依赖处理器检测溢出(从而手动检查)。
- 我所说的只是一个很久以前处理器设计类的表面记忆:我的观点是,溢出确实是导致这个设计决策的一个问题,但可能不是因为这个回答者暗示的原因。
- @aaronls:校验算术总是可以被仿真的——老实说,我甚至不知道在整数的汇编级别上它是什么样子的,但是您当然可以添加校验。
- 这种推理是错误的…试着用long代替int,用int代替byte,比如long v = int.MaxValue + 1;或者可能是double x = 5 / 2的老问题……
C.*
ECMA-334规定,添加仅在int+int、uint+uint、long+long和ulong+ulong(ECMA-334 14.7.4)上定义为合法。因此,这些是关于14.4.2的候选操作。由于存在从字节到int、uint、long和ulong的隐式强制转换,因此所有附加函数成员都是14.4.2.1中适用的函数成员。我们必须找到14.4.2.3中规则的最佳隐式投射:
铸造(c1)至int(t1)优于铸造(c2)至uint(t2)或ulong(t2),因为:
- 如果t1是int,t2是uint或ulong,c1是更好的转换。
将(c1)转换为int(t1)比将(c2)转换为long(t2)要好,因为存在从int到long的隐式转换:
- 如果存在从T1到T2的隐式转换,并且不存在从T2到T1的隐式转换,则C1是更好的转换。
因此,使用int+int函数,该函数返回int。
这是一个很长的路来说,它埋在很深的C规格。
CLI
CLI仅在6种类型(int32、native int、int64、f、o和&;)上运行。(ECMA-335分区3第1.5节)
字节(int8)不是这些类型中的一种,并且在加法前自动强制为int32。(ECMA-335分区3第1.6节)
- ECMA只指定那些特定的操作不会阻止语言实现其他规则。vb.net将有助于在不进行强制转换的情况下允许byte3 = byte1 And byte2,但如果int1 = byte1 + byte2生成的值超过255,则不会引发运行时异常。我不知道任何语言是否允许byte3 = byte1+byte2并在超过255时抛出异常,但如果int1 = byte1+byte2生成的值在256-510范围内,则不会抛出异常。
表示添加字节和将结果截断回一个字节效率低下的回答是错误的。x86处理器有专门为8位数量的整数运算设计的指令。
事实上,对于x86/64处理器,由于操作数前缀字节必须解码,因此执行32位或16位操作的效率低于64位或8位操作。在32位机器上,执行16位操作需要同样的惩罚,但是仍然有用于8位操作的专用操作码。
许多RISC架构都有类似的本地字/字节高效指令。那些通常没有存储并转换为某个位长度的有符号值的。
换句话说,这个决定必须基于对字节类型的感知,而不是由于硬件的底层效率低下。
- 酷。我不知道。
- +1;如果这种感觉每次都没有错的话,我会移动,或者用C表示两个字节……
- 截断结果不应该有任何性能成本。在x86程序集中,这只是从寄存器中复制一个字节或从寄存器中复制四个字节之间的区别。
- @乔纳塔纳伦。具有讽刺意味的是,唯一的区别是,在执行扩大转换时。当前设计在执行扩展指令(有符号扩展或无符号扩展)时会导致性能损失。
- "对字节类型的理解"——这可以解释byte和char的这种行为,但不能解释short,后者在语义上显然是一个数字。
我记得有一次读过乔恩·斯基特的文章(现在找不到了,我会继续找)关于字节实际上如何不会重载+操作符。事实上,当在示例中添加两个字节时,每个字节实际上都被隐式转换为int。结果显然是int。至于为什么这样设计,我将等待jon skeet自己发布:)
编辑:找到了!关于这个主题的很好的信息。
这是因为溢出和携带。
如果添加两个8位数字,它们可能溢出到第9位。
例子:
1 2 3 4
| 1111 1111
+ 0000 0001
-----------
1 0000 0000 |
我不确定,但我假设ints、longs、anddoubles有更多的空间,因为它们的尺寸相当大。此外,它们是4的倍数,这对计算机处理效率更高,因为内部数据总线的宽度为4字节或32位(64位现在越来越普遍)。字节和短字节效率稍低,但它们可以节省空间。
- 但是较大的数据类型不遵循相同的行为。
- 溢出问题是一个旁白。如果您将逻辑应用到语言中,那么所有数据类型在加法运算之后都会返回一个更大的数据类型,这是绝对不可能的。int+int=int,long+long=long。我认为这个问题与不一致有关。
- 同意,我重新阅读并编辑了我的文章。
- 这是我的第一个想法,但是为什么不int+int=long?所以我不赞成"可能溢出"的论点…然而咧嘴笑。
- 哦,关于"可能溢出"的参数,为什么不byte+byte=short?
- a)根据c的规则,为什么它的工作方式是这样的?请看下面我的答案。b)为什么设计成这样?可能只是对可用性的考虑,基于对大多数人使用整数和字节方式的主观判断。
- @约瑟夫:规范规定,在所有不涉及非常数右移的整数表达式都可以用符合最大有符号整数类型的所有中间结果进行计算的情况下,表达式是否会产生与使用此类类型进行计算时相同的结果?编译器不应该很难确定每个阶段所需的最大可能类型(例如,如果指定要包装的整数,int1=int2+int3;可以使用32位,但int1=(int2+int3)/2;不应该使用,除非它可以保留‘进位’)。
- @约瑟夫:事实上,longVar &= ~0x80000000的工作方式与longVar &= 0x40000000或longVar &= 0x100000000的工作方式非常不同,这在我看来是规范中的一个重大缺陷。此外,如果一种语言在给较小的变量分配较大的数时会发出嘎嘎声,它应该把两种类型都有符号或都没有符号的x & y的结果看作是较小的输入类型;如果一个有符号,而另一个没有符号,则结果应该是无符号类型。
从C语言规范1.6.7.5 7.2.6.2二进制数字升级中,如果不能将两个操作数放入其他几个类别中,它会将两个操作数转换为int。我的猜测是,它们没有重载+运算符以字节作为参数,但希望它能正常工作,所以它们只使用int数据类型。
C语言规范
我的怀疑是C实际上是在调用在int上定义的operator+(它返回一个int,除非你在checked块中),并隐式地将你的bytes和shorts都转换成ints。这就是为什么行为看起来不一致的原因。
- 它将两个字节推送到堆栈上,然后调用"add"命令。在il中,添加"eats"这两个值并用int替换它们。
这可能是语言设计者的一个实际决定。毕竟,int是一个int32,一个32位有符号整数。每当对小于int的类型执行整数操作时,大多数32位CPU都会将其转换为32位有符号int。再加上小整数溢出的可能性,这可能就决定了交易的成败。它省去了持续检查流量过/不足的繁琐工作,当字节上表达式的最终结果将在范围内时,尽管在某个中间阶段它将超出范围,但您仍然可以得到正确的结果。
另一种想法是:必须模拟这些类型上的过流/欠流,因为它不会在最可能的目标CPU上自然发生。何苦?
这在很大程度上是我关于这个主题的答案,首先提交给这里的一个类似问题。
在默认情况下,所有整数小于Int32的操作在计算之前都会四舍五入到32位。结果为Int32的原因只是简单地保留计算后的结果。如果检查msil算术操作码,则它们操作的唯一整数数字类型是Int32和Int64。这是"按设计"。
如果您希望结果返回到int16格式,那么如果您执行了强制转换代码,或者编译器(在底层)发出转换,则与此无关。
例如,要执行Int16算术运算:
1 2 3
| short a = 2, b = 3;
short c = (short) (a + b); |
这两个数字将扩展到32位,进行相加,然后截断为16位,这就是微软想要的。
使用短(或字节)的优势主要在于存储大量数据(图形数据、流媒体等)的情况。
我测试了byte和int之间的性能。使用int值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Program
{
private int a,b,c,d,e,f ;
public Program ()
{
a = 1;
b = 2;
c = (a + b );
d = (a - b );
e = (b / a );
f = (c * b );
}
static void Main (string[] args )
{
int max = 10000000;
DateTime start = DateTime .Now;
Program [] tab = new Program [max ];
for (int i = 0; i < max ; i ++)
{
tab [i ] = new Program ();
}
DateTime stop = DateTime .Now;
Debug .WriteLine(stop .Subtract(start ).TotalSeconds);
}
} |
字节值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Program
{
private byte a,b,c,d,e,f ;
public Program ()
{
a = 1;
b = 2;
c = (byte)(a + b );
d = (byte)(a - b );
e = (byte)(b / a );
f = (byte)(c * b );
}
static void Main (string[] args )
{
int max = 10000000;
DateTime start = DateTime .Now;
Program [] tab = new Program [max ];
for (int i = 0; i < max ; i ++)
{
tab [i ] = new Program ();
}
DateTime stop = DateTime .Now;
Debug .WriteLine(stop .Subtract(start ).TotalSeconds);
}
} |
结果如下:字节:3.57s 157mo,3.71s 171mo,3.74s 168mo,CPU~=30%利息:4.05s 298mo,3.92s 278mo,4.28 294mo,CPU~=27%结论:字节使用更多的CPU,但它占用了LES内存,而且速度更快(可能是因为要分配的字节更少)
没有为字节定义加法。因此,它们被强制转换为int进行加法运算。大多数数学运算和字节都是这样。(请注意,这是以前的语言,我假设它在今天是正确的)。
我认为这是一个设计决策,关于哪个操作更常见…如果byte+byte=byte,那么当结果需要int时,可能会有更多的人被强制转换成int。
- 我一度被另一种方式所困扰:)我似乎总是需要字节结果,所以我总是需要强制转换。
- 但你不必强制转换为int。转换是隐式的。只有另一种方式是明确的。
- @尼基,我想你不明白我的回答。如果添加两个字节将产生一个字节,为了防止溢出,必须在添加之前将操作数(而不是结果)强制转换为int。
来自.NET框架代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| // bytes
private static object AddByte(byte Left, byte Right)
{
short num = (short) (Left + Right);
if (num > 0xff)
{
return num;
}
return (byte) num;
}
// shorts (int16)
private static object AddInt16(short Left, short Right)
{
int num = Left + Right;
if ((num <= 0x7fff) && (num >= -32768))
{
return (short) num;
}
return num;
} |
使用.NET 3.5及更高版本进行简化:
1 2 3 4 5 6 7
| public static class Extensions
{
public static byte Add(this byte a, byte b)
{
return (byte)(a + b);
}
} |
现在你可以做到:
1 2
| byte a = 1, b = 2, c;
c = a.Add(b); |
除了所有其他伟大的评论,我想我会加一点小道消息。很多评论都想知道为什么in t、long和几乎所有其他数字类型都不遵循这个规则……返回一个"更大"的类型来响应算术。
很多答案都与性能有关(32位比8位快)。实际上,8位数字对32位CPU来说仍然是32位数字……即使添加两个字节,CPU操作的数据块也将是32位,不管怎样……所以添加ints不会比添加两个字节更快……对CPU来说是一样的。现在,添加两个整数比在32位处理器上添加两个长整型更快,因为添加两个长整型需要更多的微操作,因为您处理的数字比处理器字宽。
我认为导致字节算术产生整数的根本原因是非常清楚和直截了当的:8它只是没有走多远!:d有8位,无符号范围为0-255。这不是一个很好的工作空间……在算术中使用字节限制的可能性很高。但是,在使用int、long或double等时,您将耗尽位的机会非常低……足够低,我们很少遇到需要更多位的情况。
字节到int的自动转换是合乎逻辑的,因为字节的小数位数太小了。从int到long、float到double等的自动转换是不符合逻辑的,因为这些数字具有显著的规模。
- 这仍然不能解释为什么byte - byte返回int,或者为什么它们不投射到short上……
- 为什么希望加法返回与减法不同的类型?如果byte + byte返回int,因为255+的任何值大于一个字节所能容纳的值,那么从返回类型一致性的角度来看,让任何字节减去任何其他字节返回除int以外的任何值是没有意义的。
- 我不会的,这只是说明上述原因可能不正确。如果必须对结果进行"拟合",那么byte减法将返回byte,字节加法将返回short(byte+byte将始终适合于short)。如果像你说的那样是关于一致性的话,那么short对于两种操作仍然足够,而不是int。很明显,原因是多种多样的,并非所有原因都经过深思熟虑。或者,下面给出的性能原因可能更准确。