在java中,使用byte或short而不是int和float而不是double更有效吗?
Posted
技术标签:
【中文标题】在java中,使用byte或short而不是int和float而不是double更有效吗?【英文标题】:In java, is it more efficient to use byte or short instead of int and float instead of double? 【发布时间】:2013-01-09 23:21:46 【问题描述】:我注意到我总是使用 int 和 doubles,无论数字需要多大或多小。那么在java中,使用byte
或short
而不是int
和float
而不是double
更有效吗?
所以假设我有一个包含大量整数和双精度数的程序。如果我知道这个数字合适,是否值得将我的整数更改为字节或短裤?
我知道 java 没有无符号类型,但如果我知道这个数字只是正数,我还能做些什么吗?
我所说的高效主要是指处理。我假设如果所有变量都是一半大小,那么垃圾收集器会快得多,并且计算也可能会更快一些。 (我想因为我正在使用 android,所以我也需要有点担心 ram)
(我假设垃圾收集器只处理对象而不是原始对象,但仍会删除废弃对象中的所有原始对象,对吧?)
我用我拥有的一个小型 android 应用程序进行了尝试,但并没有真正注意到有什么不同。 (虽然我没有“科学地”测量任何东西。)
我认为它应该更快更高效是不是错了?我不想经历并改变一个庞大的程序中的所有内容,以发现我浪费了我的时间。
当我开始一个新项目时,是否值得从头开始? (我的意思是我认为每一点都会有所帮助,但如果是这样的话,为什么似乎没有人这样做。)
【问题讨论】:
【参考方案1】:我认为它应该更快更高效是不是错了?我不想经历并改变一个庞大的程序中的所有内容,以发现我浪费了我的时间。
简答
是的,你错了。在大多数情况下,它在使用的空间方面差别不大。
不值得尝试优化...除非您有明确的证据表明需要优化。如果您确实需要特别优化对象字段的内存使用,您可能需要采取其他(更有效的)措施。
更长的答案
Java 虚拟机使用(实际上)是 32 位原始单元大小的倍数的偏移量对堆栈和对象字段进行建模。因此,当您将局部变量或对象字段声明为(例如)byte
时,变量/字段将存储在 32 位单元格中,就像 int
一样。
这有两个例外:
long
和 double
值需要 2 个原始 32 位单元
原始类型数组以压缩形式表示,因此(例如)一个字节数组每个 32 位字包含 4 个字节。
因此 可能 值得优化使用 long
和 double
... 以及大型基元数组。但一般不会。
理论上,JIT 可能能够优化这一点,但实际上我从未听说过有这样的 JIT。一个障碍是 JIT 通常在创建正在编译的类的实例之后才能运行。如果 JIT 优化了内存布局,你可以有两个(或更多)同一个类的对象“风格”......这将带来巨大的困难。
重温
查看@meriton 答案中的基准测试结果,似乎使用short
和byte
而不是int
会导致乘法性能下降。事实上,如果你孤立地考虑这些操作,那么惩罚是显着的。 (你不应该孤立地考虑它们......但这是另一个话题。)
我认为解释是 JIT 可能在每种情况下都使用 32 位乘法指令进行乘法运算。但在byte
和short
的情况下,它会在每次循环迭代中执行额外 指令将中间32 位值转换为byte
或short
。 (理论上,可以在循环结束时进行一次转换......但我怀疑优化器是否能够解决这个问题。)
无论如何,这确实指出了切换到short
和byte
作为优化的另一个问题。它可能会使性能更差 ...在算术和计算密集型算法中。
次要问题
我知道 java 没有无符号类型,但如果我知道这个数字只是正数,我还能做些什么吗?
没有。无论如何,不是在性能方面。 (Integer
、Long
等中有一些方法可以将int
、long
等处理为无符号。但这些并没有带来任何性能优势。这不是他们的目的。)
(我假设垃圾收集器只处理对象而不是原始对象,但仍会删除废弃对象中的所有原始对象对吧?)
正确。对象的字段是对象的一部分。当对象被垃圾收集时,它就会消失。同样,当收集阵列时,阵列的单元格也会消失。当字段或单元格类型是原始类型时,值将存储在字段/单元格中......它是对象/数组的一部分......并且已被删除。
【讨论】:
+1 除非您有明确的性能问题证据,否则不要优化 呃,为什么JVM要等JIT编译才能打包一个类的内存布局呢?由于字段的类型被写入类文件,JVM不能在类加载时选择内存布局,然后将字段名称解析为字节而不是字偏移? @meriton - 我很确定对象布局 在类加载时确定,之后它们不会改变。请参阅我的答案的“细则”部分。如果在 JITed 代码时实际内存布局发生了变化,那么 JVM 将很难处理。 (当我说 JIT 可能 优化布局时,这是假设和不切实际的......这可以解释为什么我从未听说过 JIT 实际这样做。) 我知道。我只是想指出,即使在创建对象后很难更改内存布局,但 JVM 可能仍会在此之前优化内存布局,即在类加载时。换句话说,JVM 规范描述了带有单词偏移量的 JVM 的行为并不一定意味着 JVM 需要以这种方式实现——尽管很可能是这样。 @meriton - JVM 规范正在讨论本地框架/对象中的“虚拟机字偏移”。未指定如何将这些映射到物理机器偏移量。实际上,它不能指定它......因为可能存在特定于硬件的字段对齐要求。【参考方案2】:这取决于 JVM 的实现以及底层硬件。大多数现代硬件不会从内存(甚至从一级缓存)中获取单个字节,即使用较小的原始类型通常不会减少内存带宽消耗。同样,现代 CPU 的字长为 64 位。他们可以对更少的位执行操作,但是通过丢弃额外的位来工作,这也不是更快。
唯一的好处是较小的原始类型可以导致更紧凑的内存布局,尤其是在使用数组时。这样可以节省内存,从而可以提高引用的局部性(从而减少缓存未命中的数量)并减少垃圾收集开销。
不过,一般来说,使用较小的原始类型并不快。
为了证明这一点,请看以下基准:
public class Benchmark
public static void benchmark(String label, Code code)
print(25, label);
try
for (int iterations = 1; ; iterations *= 2) // detect reasonable iteration count and warm up the code under test
System.gc(); // clean up previous runs, so we don't benchmark their cleanup
long previouslyUsedMemory = usedMemory();
long start = System.nanoTime();
code.execute(iterations);
long duration = System.nanoTime() - start;
long memoryUsed = usedMemory() - previouslyUsedMemory;
if (iterations > 1E8 || duration > 1E9)
print(25, new BigDecimal(duration * 1000 / iterations).movePointLeft(3) + " ns / iteration");
print(30, new BigDecimal(memoryUsed * 1000 / iterations).movePointLeft(3) + " bytes / iteration\n");
return;
catch (Throwable e)
throw new RuntimeException(e);
private static void print(int desiredLength, String message)
System.out.print(" ".repeat(Math.max(1, desiredLength - message.length())) + message);
private static long usedMemory()
return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
@FunctionalInterface
interface Code
/**
* Executes the code under test.
*
* @param iterations
* number of iterations to perform
* @return any value that requires the entire code to be executed (to
* prevent dead code elimination by the just in time compiler)
* @throws Throwable
* if the test could not complete successfully
*/
Object execute(int iterations);
public static void main(String[] args)
benchmark("long[] traversal", (iterations) ->
long[] array = new long[iterations];
for (int i = 0; i < iterations; i++)
array[i] = i;
return array;
);
benchmark("int[] traversal", (iterations) ->
int[] array = new int[iterations];
for (int i = 0; i < iterations; i++)
array[i] = i;
return array;
);
benchmark("short[] traversal", (iterations) ->
short[] array = new short[iterations];
for (int i = 0; i < iterations; i++)
array[i] = (short) i;
return array;
);
benchmark("byte[] traversal", (iterations) ->
byte[] array = new byte[iterations];
for (int i = 0; i < iterations; i++)
array[i] = (byte) i;
return array;
);
benchmark("long fields", (iterations) ->
class C
long a = 1;
long b = 2;
C[] array = new C[iterations];
for (int i = 0; i < iterations; i++)
array[i] = new C();
return array;
);
benchmark("int fields", (iterations) ->
class C
int a = 1;
int b = 2;
C[] array = new C[iterations];
for (int i = 0; i < iterations; i++)
array[i] = new C();
return array;
);
benchmark("short fields", (iterations) ->
class C
short a = 1;
short b = 2;
C[] array = new C[iterations];
for (int i = 0; i < iterations; i++)
array[i] = new C();
return array;
);
benchmark("byte fields", (iterations) ->
class C
byte a = 1;
byte b = 2;
C[] array = new C[iterations];
for (int i = 0; i < iterations; i++)
array[i] = new C();
return array;
);
benchmark("long multiplication", (iterations) ->
long result = 1;
for (int i = 0; i < iterations; i++)
result *= 3;
return result;
);
benchmark("int multiplication", (iterations) ->
int result = 1;
for (int i = 0; i < iterations; i++)
result *= 3;
return result;
);
benchmark("short multiplication", (iterations) ->
short result = 1;
for (int i = 0; i < iterations; i++)
result *= 3;
return result;
);
benchmark("byte multiplication", (iterations) ->
byte result = 1;
for (int i = 0; i < iterations; i++)
result *= 3;
return result;
);
在我的 Intel Core i7 CPU @ 3.5 GHz 上使用 OpenJDK 14 运行,打印结果:
long[] traversal 3.206 ns / iteration 8.007 bytes / iteration
int[] traversal 1.557 ns / iteration 4.007 bytes / iteration
short[] traversal 0.881 ns / iteration 2.007 bytes / iteration
byte[] traversal 0.584 ns / iteration 1.007 bytes / iteration
long fields 25.485 ns / iteration 36.359 bytes / iteration
int fields 23.126 ns / iteration 28.304 bytes / iteration
short fields 21.717 ns / iteration 20.296 bytes / iteration
byte fields 21.767 ns / iteration 20.273 bytes / iteration
long multiplication 0.538 ns / iteration 0.000 bytes / iteration
int multiplication 0.526 ns / iteration 0.000 bytes / iteration
short multiplication 0.786 ns / iteration 0.000 bytes / iteration
byte multiplication 0.784 ns / iteration 0.000 bytes / iteration
如您所见,唯一显着的速度节省发生在遍历大型数组时;使用较小的对象字段产生的好处可以忽略不计,而且小数据类型的计算实际上会稍微慢一些。
总体而言,性能差异很小。优化算法远比选择基元类型重要。
【讨论】:
与其说“最值得注意的是使用数组”,我认为更简单的说法是short
和byte
在存储在足够大的数组中时更有效(数组越大,效率差异越大;byte[2]
的效率可能比int[2]
更高或更低,但无论哪种方式都不够重要),但单个值更有效地存储为int
。
我检查的内容:这些基准总是使用 int ('3') 作为因子或赋值操作数(循环变体,然后强制转换)。我所做的是根据左值类型使用类型因子/赋值操作数: int mult 76.481 ns int mult (typed) 72.581 ns short mult 87.908 ns short mult (typed) 90.772 ns byte mult 87.859 ns byte mult (typed) 89.524 ns int[] trav 88.905 ns int[] trav (typed) 89.126 ns short[] trav 10.563 ns short[] trav (typed) 10.039 ns byte[] trav 8.356 ns byte[] trav (typed) 8.338 ns 我想有一个很多不必要的铸造。这些测试是在 android 选项卡上运行的。【参考方案3】:
如果大量使用byte
而不是int
可以提高性能。这是一个实验:
import java.lang.management.*;
public class SpeedTest
/** Get CPU time in nanoseconds. */
public static long getCpuTime()
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
return bean.isCurrentThreadCpuTimeSupported() ? bean
.getCurrentThreadCpuTime() : 0L;
public static void main(String[] args)
long durationTotal = 0;
int numberOfTests=0;
for (int j = 1; j < 51; j++)
long beforeTask = getCpuTime();
// MEASURES THIS AREA------------------------------------------
long x = 20000000;// 20 millions
for (long i = 0; i < x; i++)
TestClass s = new TestClass();
// MEASURES THIS AREA------------------------------------------
long duration = getCpuTime() - beforeTask;
System.out.println("TEST " + j + ": duration = " + duration + "ns = "
+ (int) duration / 1000000);
durationTotal += duration;
numberOfTests++;
double average = durationTotal/numberOfTests;
System.out.println("-----------------------------------");
System.out.println("Average Duration = " + average + " ns = "
+ (int)average / 1000000 +" ms (Approximately)");
该类测试创建新TestClass
的速度。每个测试执行 2000 万次,总共有 50 次测试。
这是测试类:
public class TestClass
int a1= 5;
int a2= 5;
int a3= 5;
int a4= 5;
int a5= 5;
int a6= 5;
int a7= 5;
int a8= 5;
int a9= 5;
int a10= 5;
int a11= 5;
int a12=5;
int a13= 5;
int a14= 5;
我运行了SpeedTest
课程,最后得到了这个:
Average Duration = 8.9625E8 ns = 896 ms (Approximately)
现在我在 TestClass 中将整数更改为字节并再次运行它。结果如下:
Average Duration = 6.94375E8 ns = 694 ms (Approximately)
我相信这个实验表明,如果你要实例化大量变量,使用 byte 代替 int 可以提高效率
【讨论】:
请注意,此基准仅衡量与分配和构建相关的成本,并且仅适用于具有大量单个字段的类的情况。如果在字段上执行算术/更新操作,@meriton 的结果表明byte
可能比int
慢
没错,我应该用措辞更好地澄清它。【参考方案4】:
byte 通常被认为是 8 位。 short 一般认为是 16 位。
在一个“纯”的环境中,它不是 java 的,因为字节、长、短和其他有趣的东西通常对你隐藏,字节可以更好地利用空间。
但是,您的计算机可能不是 8 位的,也可能不是 16 位的。这意味着 特别是要获得 16 或 8 位,它需要诉诸“诡计”,这会浪费时间来假装它有能力在需要时访问这些类型。
此时,这取决于硬件是如何实现的。然而,从我一直很艰难, 最好的速度是通过将内容存储在适合您的 CPU 使用的块中来实现的。一个 64 位处理器喜欢处理 64 位元素,而任何低于此的东西通常需要“工程魔法”来假装它喜欢处理它们。
【讨论】:
我不确定你所说的“工程魔法”是什么意思......大多数/所有现代处理器都有快速指令来加载一个字节并对其进行符号扩展,以存储一个全角寄存器,并在全宽寄存器的一部分中进行字节宽度或短宽度算术。如果您是对的,那么在可行的情况下,在 64 位处理器上将所有整数替换为长整数是有意义的。 我可以想象这是真的。我只记得在我们使用的摩托罗拉 68k 模拟器中,大多数操作可以使用 16 位值,而不能使用 32 位或 64 位。我在想这意味着系统有一个可以最佳获取的首选值大小。虽然我可以想象现代 64 位处理器可以同样轻松地获取 8 位、16 位、32 位和 64 位,但在这种情况下,这不是问题。感谢您指出这一点。 "... 通常被认为是..." - 实际上,明确、明确地>>指定 大量处理器甚至使用相同数量的周期来操作和访问非字大小的数据,因此除非您在特定的 JVM 和平台上进行测量,否则不必担心。 我试图笼统地说。也就是说,我实际上并不确定 Java 在字节大小方面的标准,但在这一点上,我非常确信,如果有任何异端决定非 8 位字节,Java 不会想用 10 英尺的柱子碰它们。但是,一些处理器需要多字节对齐,如果 Java 平台支持它们,它需要做的事情更慢以适应处理这些较小的类型,或者用比您要求的更大的表示法来神奇地表示它们。这总是比其他类型更喜欢 int,因为它总是使用系统最喜欢的大小。【参考方案5】:short/byte/char 性能较差的原因之一是缺乏对这些数据类型的直接支持。通过直接支持,这意味着 JVM 规范没有提及这些数据类型的任何指令集。存储、加载、添加等指令具有 int 数据类型的版本。但他们没有短/字节/字符的版本。例如。考虑下面的java代码:
void spin()
int i;
for (i = 0; i < 100; i++)
; // Loop body is empty
同样被转换成机器码如下。
0 iconst_0 // Push int constant 0
1 istore_1 // Store into local variable 1 (i=0)
2 goto 8 // First time through don't increment
5 iinc 1 1 // Increment local variable 1 by 1 (i++)
8 iload_1 // Push local variable 1 (i)
9 bipush 100 // Push int constant 100
11 if_icmplt 5 // Compare and loop if less than (i < 100)
14 return // Return void when done
现在,考虑如下将 int 更改为 short。
void sspin()
short i;
for (i = 0; i < 100; i++)
; // Loop body is empty
对应的机器码会变化如下:
0 iconst_0
1 istore_1
2 goto 10
5 iload_1 // The short is treated as though an int
6 iconst_1
7 iadd
8 i2s // Truncate int to short
9 istore_1
10 iload_1
11 bipush 100
13 if_icmplt 5
16 return
如您所见,要操作 short 数据类型,它仍然使用 int 数据类型指令版本,并在需要时将 int 显式转换为 short。现在,由于这个原因,性能会降低。
现在,不提供直接支持的原因如下:
Java虚拟机对数据提供最直接的支持 输入整数。这部分是在预期有效的实施 Java 虚拟机的操作数栈和局部变量 数组。它还受到典型中 int 数据频率的推动 程式。其他整数类型的直接支持较少。没有 存储、加载或添加指令的字节、字符或短版本, 例如。
引用自 JVM 规范存在 here(第 58 页)。
【讨论】:
这些是反汇编的字节码;即 JVM virtual 指令。它们没有被javac
编译器优化,你不能从它们中得出任何关于程序在现实生活中的表现的可靠推论。 JIT 编译器将这些字节码编译为实际本机机器指令,并在此过程中进行一些非常严肃的优化。如果要分析代码的性能,则需要检查本机代码指令。 (而且它很复杂,因为您需要考虑多级 x86_64 管道的时序行为。)
我相信java规范是供javac实现者实现的。所以我认为在那个级别上没有更多的优化。无论如何,我也可能完全错了。请分享一些参考链接以支持您的陈述。
这里有一个事实来支持我的说法。您不会找到任何(可靠的)时序数据来告诉您每个 JVM 字节码指令需要多少时钟周期。当然不是由 Oracle 或其他 JVM 供应商发布的。另外,请阅读***.com/questions/1397009
我确实找到了一篇旧的(2008 年)论文,其中有人试图开发一个独立于平台的模型来预测字节码序列的性能。他们声称,与 RDTSC 测量结果相比,他们的预测偏差了 25%....奔腾。他们在禁用 JIT 编译的情况下运行 JVM!参考:sciencedirect.com/science/article/pii/S1571066108004581
不,不是。您的答案是根据字节码做出断言。正如我的 cmets 所说,字节码不允许您推断性能,因此您的断言不是基于逻辑上合理的基础。现在,如果您转储本机代码并分析它们并看到额外的本机指令来进行短 长转换,那将是支持证据。但不是这个。据我们所知,i2s
字节码指令可以被 JIT 编译器优化掉【参考方案6】:
差异几乎不明显!这更多的是设计、适当性、统一性、习惯等问题……有时只是品味问题。当您关心的只是您的程序启动并运行并且用float
替换int
不会损害正确性时,我认为选择一个或另一个没有任何优势,除非您可以证明使用任何一种类型都会改变性能。根据 2 或 3 个字节不同的类型调整性能确实是您最不应该关心的事情; Donald Knuth 曾经说过:“过早的优化是万恶之源”(不确定是他,如果你有答案,请编辑)。
【讨论】:
Nit: Afloat
不能代表int
可以的所有整数; int
也不能代表 float
可以的任何非整数值。也就是说,虽然所有 int 值都是 long 值的子集,但 int 不是 float 的子集,而 float 不是 int 的子集。跨度>
我希望回答者打算写substituting a float for a double
,如果是这样,回答者应该编辑答案。如果不是回答者应该因为@pst 概述的原因和许多其他原因而羞愧地低头并回到基础。
@HighPerformanceMark 不,我把 int 和 float 因为这就是我的想法。尽管我在想 C,但我的回答并不是针对 Java 的……它是通用的。你得到的意思是评论。以上是关于在java中,使用byte或short而不是int和float而不是double更有效吗?的主要内容,如果未能解决你的问题,请参考以下文章
为啥java虚拟机jvm要把byte和short的运算都转为int
在JAVA中程序byte,short,int,long它们的用是啥