为什么Java API使用int
,而short
甚至byte
就足够了?
示例:类 Calendar
中的 DAY_OF_WEEK
字段使用 int
。
如果差异太小,那么为什么这些数据类型(short
,int
)存在呢?
一些原因已经指出了。例如,"...(几乎)对字节、短的所有操作都将把这些原语提升为 int"。但是,显而易见的下一个问题是:为什么这些类型被提升为int
?
因此,更深入地说:答案可能只是与Java虚拟机指令集有关。如 Java 虚拟机规范中的表中所述,所有积分算术运算(如加法、除法等)仅适用于类型 int
和类型 long
,而不适用于较小的类型。
题外话:较小的类型(byte
和short
)基本上只用于数组。像 new byte[1000]
这样的数组需要 1000 个字节,像 new int[1000]
这样的数组需要 4000 个字节)
现在,当然,人们可以说"...显而易见的下一个问题是:为什么这些说明只提供给int
(和long
)?
上面提到的JVM规范中提到了一个原因:
如果每个类型化指令都支持 Java 虚拟机的所有运行时数据类型,则指令数将多于一个字节所能表示的指令数
此外,Java 虚拟机可以被视为真实处理器的抽象。为较小的类型引入专用的算术逻辑单元是不值得的:它需要额外的晶体管,但它仍然只能在一个时钟周期内执行一个加法。设计JVM时的主要架构是32位,正好适合32位int
。(涉及 64 位long
值的操作作为特殊情况实现)。
(注意:最后一段有点过于简单,考虑到可能的矢量化等,但应该给出基本思想,不要太深入地探讨处理器设计主题)
编辑:一个简短的附录,侧重于问题中的示例,但在更一般的意义上:人们还可以问使用较小的类型存储字段是否没有好处。例如,人们可能会认为可以通过Calendar.DAY_OF_WEEK
存储为byte
来节省内存。但是在这里,Java类文件格式开始发挥作用:类文件中的所有字段至少占用一个"插槽",其大小为一int
(32位)。("宽"字段,double
和long
,占据两个插槽)。因此,显式声明字段为 short
或 byte
也不会节省任何内存。
(几乎)byte
上的所有操作,short
会将它们提升为int
,例如,你不能写:
short x = 1;
short y = 2;
short z = x + y; //error
算术在使用int
时更容易和直接,不需要铸造。
就空间而言,它几乎没有区别。 byte
和short
会使事情复杂化,我认为这种微优化不值得,因为我们谈论的是固定数量的变量。
当您为嵌入式设备编程或处理文件/网络时,byte
是相关且有用的。这些原语也是有限的,如果将来计算可能超过其限制怎么办?尝试考虑Calendar
类的扩展,这可能会演变出更大的数字。
另请注意,在 64 位处理器中,局部变量将保存在寄存器中,不会使用任何资源,因此使用 int
、short
和其他原语根本不会有任何区别。此外,许多 Java 实现对齐变量*(和对象)。
* byte
和 short
如果它们是局部变量、类变量甚至实例变量,则占用与 int
相同的空间。为什么?因为在(大多数)计算机系统中,变量地址是对齐的,所以例如,如果你使用一个字节,你实际上最终会得到两个字节 - 一个用于变量本身,另一个用于填充。
另一方面,在数组中,byte
取 1 个字节,short
取 2 个字节,int
取 4 个字节,因为在数组中,只有开头和结尾必须对齐。如果您想使用例如System.arraycopy()
,这将有所不同,那么您将真正注意到性能差异。
因为与短裤相比,使用整数时的算术运算更容易。假设常量确实是通过short
值建模的。然后,您必须以这种方式使用 API:
short month = Calendar.JUNE;
month = month + (short) 1; // is july
请注意显式强制转换。短值在算术运算中使用时隐式提升为int
值。(在操作数堆栈上,短裤甚至表示为 int。使用起来会非常麻烦,这就是为什么常量通常首选int
值的原因。
与此相比,存储效率的提高很小,因为仅存在固定数量的此类常量。我们谈论的是 40 个常量。将它们的存储从int
更改为short
将使您40 * 16 bit = 80 byte
安全。请参阅此答案以获取进一步参考。
虚拟机的设计复杂性取决于它可以执行多少种操作。 拥有像"乘法"这样的指令的四个实现(32 位整数、64 位整数、32 位浮点数和 64 位浮点各一个)比除了上述之外,还有较小的数值类型的版本更容易。 一个更有趣的设计问题是为什么应该有四种类型,而不是更少(使用 64 位整数执行所有整数计算和/或使用 64 位浮点值执行所有浮点计算)。 使用 32 位整数的原因是,Java 预计将在许多平台上运行,在这些平台上,32 位类型的操作速度与 16 位或 8 位类型一样快,但对 64 位类型的操作会明显变慢。 即使在使用 16 位类型的平台上,使用 32 位数量的额外成本也会被仅使用 32 位类型提供的简单性所抵消。
至于对 32 位值执行浮点计算,其优势不太明显。 在某些平台上,可以通过将所有操作数转换为更高精度的类型,添加它们,然后将结果转换回 32 位浮点数进行存储,从而最快速地执行像 float a=b+c+d;
这样的计算。 在其他平台上,使用 32 位浮点值执行所有计算会更有效。 Java的创建者决定,应该要求所有平台都以相同的方式做事,并且他们应该支持32位浮点计算比长浮点计算更快的硬件平台,尽管这严重降低了PC在典型PC上以及许多没有浮点单元的机器上浮点数学的速度和精度。 请注意,顺便说一句,根据 b、c 和 d 的值,在计算上述float a=b+c+d;
表达式时,使用更高精度的中间计算有时会产生比以float
精度计算的所有中间操作数更准确的结果,但有时会产生一个稍微不准确的值。 无论如何,Sun决定一切都应该以同样的方式完成,他们选择使用最小精度的float
值。
请注意,当大量数据类型一起存储在一个数组中时,较小数据类型的主要优势变得明显;即使拥有小于 64 位类型的单个变量没有优势,拥有可以更紧凑地存储较小值的数组也是值得的;让局部变量成为byte
而不是long
可以节省七个字节;让 1,000,000 个数字的数组保存每个数字作为byte
而不是long
波浪 7,000,000 字节。 由于每种数组类型只需要支持几个操作(最明显的是读取一个项目、存储一个项目、复制数组中的一系列项目或将一系列项目从一个数组复制到另一个数组),因此拥有更多数组类型增加的复杂性并不像拥有更多类型的直接可用的离散数值的复杂性那么严重。
如果你使用整数常量存储在它们适合的最小类型中的哲学,那么Java将面临一个严重的问题:每当程序员使用整数常量编写代码时,他们必须仔细注意他们的代码以检查常量的类型是否重要,如果是这样,请在文档中查找类型和/或进行所需的任何类型的转换。
那么,既然我们已经概述了一个严重的问题,您希望通过这种理念获得什么好处?如果该更改的唯一运行时可观察到的影响是当您通过反射查找常量时获得的类型,我不会感到惊讶。(当然,任何错误都是由懒惰/不知情的程序员引入的,没有正确考虑常量的类型)
权衡利弊很容易:这是一个糟糕的哲学。
实际上,有一个小优势。如果您有
class MyTimeAndDayOfWeek {
byte dayOfWeek;
byte hour;
byte minute;
byte second;
}
那么在典型的JVM上,它需要的空间与包含单个int
的类一样多。内存消耗四舍五入到下一个 8 或 16 字节的倍数(IIRC,这是可配置的),因此真正节省的情况相当罕见。
如果相应的Calendar
方法返回byte
,则此类将稍微更容易使用。但是没有这样的Calendar
方法,只有get(int)
由于其他字段而必须返回int
的方法。对较小类型的每个操作都会提升到int
,因此您需要大量的铸造。
最有可能的是,你要么放弃并切换到int
,要么写像这样的二传手。
void setDayOfWeek(int dayOfWeek) {
this.dayOfWeek = checkedCastToByte(dayOfWeek);
}
那么无论如何,DAY_OF_WEEK
的类型并不重要。
使用小于 CPU 总线大小的变量意味着需要更多的周期。例如,在更新内存中的单个字节时,64 位 CPU 需要读取整个 64 位字,仅修改更改的部分,然后写回结果。
此外,当变量存储在寄存器中时,使用较小的数据类型需要开销,因为要显式考虑较小数据类型的行为。由于无论如何都使用整个寄存器,因此对方法参数和局部变量使用较小的数据类型没有任何好处。
尽管如此,这些数据类型对于表示需要特定宽度的数据结构(如网络数据包)或节省大型数组中的空间(牺牲速度)可能很有用。