在Java中,使用byte或short代替int和float代替double效率更高吗?


91

我注意到我总是使用int并加倍,无论数字的大小是多少。因此,在Java中,使用byteshort代替intfloat代替会更有效double吗?

因此,假设我有一个包含很多整数和双精度数的程序。如果我知道这个数字合适,将我的整数更改为字节或短裤是否值得?

我知道Java没有无符号类型,但是如果我知道数字只会是正数,我还能做些额外的事情吗?

高效,我主要是指处理。我认为,如果所有变量的大小都减半,那么垃圾收集器的速度将大大提高,并且计算速度也可能会更快。(我想因为我在android上工作,所以我也需要担心ram)

(我假设垃圾收集器仅处理对象,而不处理原始对象,但仍然删除废弃对象中的所有原始对象,对吗?)

我用一个小型的Android应用程序进行了尝试,但实际上并没有发现任何区别。(尽管我没有“科学地”测量任何东西。)

我以为它应该更快,更有效吗?我不希望在大型程序中进行更改,以发现自己浪费了时间。

当我开始一个新项目时,从一开始就值得做吗?(我的意思是,我认为一点点都会有所帮助,但是如果有的话,那又是一次,为什么似乎没有人这样做呢?)

Answers:


107

我以为它应该更快,更有效吗?我不希望在大型程序中进行更改,以发现自己浪费了时间。

简短答案

是的,你错了。在大多数情况下,所使用的空间几乎没有区别

这是不值得尝试优化这个......除非你有明确的证据表明需要优化。而且,如果确实需要优化对象字段的内存使用,则可能需要采取其他(更有效的)措施。

更长的答案

Java虚拟机使用偏移量(实际上是32位原始单元格大小的倍数)对堆栈和对象字段进行建模。因此,当您将局部变量或对象字段声明为(例如)a时byte,变量/字段将存储在32位单元格中,就像一样int

有两个例外:

  • longdouble值需要2个原始32位单元
  • 基本类型的数组以打包形式表示,因此(例如)字节数组每个32位字包含4个字节。

因此,它可能是值得的优化利用longdouble......和基本类型的大型阵列。但总的来说没有。

从理论上讲,JIT 可能可以对此进行优化,但是在实践中,我从未听说过这样做。一个障碍是,JIT通常无法运行,直到创建了要编译的类的实例之后。如果JIT优化了内存布局,则您可能会有两个(或更多)相同类的对象的“味道”……这将带来巨大的困难。


重访

查看@meriton答案中的基准测试结果,看来使用shortbyte而不是int乘法会导致性能下降。的确,如果孤立地考虑这些操作,那么代价将是巨大的。(您不应该孤立地考虑它们……但这是另一个主题。)

我认为这是因为JIT可能在每种情况下都使用32位乘法指令进行乘法运算。但是在byte和的short情况下,它会执行额外的指令以在每次循环迭代中将中间的32位值转换为a byteshort。(理论上,该转换可以在循环结束时完成一次,但是我怀疑优化器是否能够解决这一问题。)

无论如何,这确实指出了切换到shortbyte作为优化的另一个问题。在算术和计算密集型的算法中,这可能会使性能变差


30
除非您有明确的性能问题证据,否则+1不会优化
Bohemian

嗯,为什么JVM必须等待JIT编译来打包一个类的内存布局?由于字段的类型已写入类文件,因此JVM无法在类加载时选择内存布局,然后将字段名称解析为字节而不是字偏移量吗?
meriton

@meriton-我很确定对象布局在类加载时确定的,并且在此之后它们不会更改。请参阅我的答案的“精细印刷”部分。如果在代码被JIT时实际的内存布局发生了变化,那么JVM真的很难处理。(当我说JIT 可能优化布局时,这是假设性的和不切实际的……这可以解释为什么我从未听说过JIT实际上在做它。)
Stephen C

我知道。我只是想指出,即使创建对象后很难更改内存布局,但JVM仍可能会在此之前(即在类加载时)优化内存布局。换句话说,JVM规范使用字偏移量描述JVM的行为并不一定意味着需要以这种方式实现JVM,尽管很可能是这样。
优点

@meriton-JVM规范讨论的是本地框架/对象中的“虚拟机字信息”。未指定如何将它们映射到物理机偏移。实际上,它无法指定它...因为可能存在特定于硬件的字段对齐要求。
Stephen C

29

这取决于JVM以及底层硬件的实现。大多数现代硬件不会从内存(甚至从第一级缓存)中获取单个字节,即使用较小的原始类型通常不会减少内存带宽消耗。同样,现代CPU的字长为64位。他们可以对较少的位执行操作,但是可以通过丢弃多余的位来工作,但这也不快。

唯一的好处是,较小的原始类型可以导致更紧凑的内存布局,尤其是在使用数组时。这样可以节省内存,从而可以改善引用的局部性(从而减少缓存未命中的次数)并减少垃圾回收的开销。

但是,一般而言,使用较小的原始类型并不快。

为了证明这一点,请遵循以下基准:

package tools.bench;

import java.math.BigDecimal;

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1; 
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }   

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("int multiplication") {
                @Override int run(int iterations) throws Throwable {
                    int x = 1;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("short multiplication") {                   
                @Override int run(int iterations) throws Throwable {
                    short x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("byte multiplication") {                   
                @Override int run(int iterations) throws Throwable {
                    byte x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x *= 3;
                    }
                    return x;
                }
            },
            new Benchmark("int[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    int[] x = new int[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = i;
                    }
                    return x[x[0]];
                }
            },
            new Benchmark("short[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    short[] x = new short[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = (short) i;
                    }
                    return x[x[0]];
                }
            },
            new Benchmark("byte[] traversal") {                   
                @Override int run(int iterations) throws Throwable {
                    byte[] x = new byte[iterations];
                    for (int i = 0; i < iterations; i++) {
                        x[i] = (byte) i;
                    }
                    return x[x[0]];
                }
            },
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

在我有些旧的笔记本上打印(添加空格以调整列):

int       multiplication    1.530 ns
short     multiplication    2.105 ns
byte      multiplication    2.483 ns
int[]     traversal         5.347 ns
short[]   traversal         4.760 ns
byte[]    traversal         2.064 ns

如您所见,性能差异很小。优化算法远比选择原始类型重要。


3
与其说“使用数组时最引人注目”,不如说起来更简单,short并且byte当存储在足够重要的数组中时效率更高(数组越大,效率差异越大; a byte[2]可能会更大)或效率不如int[2],但无论如何都不够用),但将各个值更有效地存储为int
2013年

2
我检查的内容:这些基准测试始终使用int('3')作为因子或赋值操作数(循环变量,然后进行强制转换)。我所做的是根据左值类型使用类型化因子/赋值操作数:int mult 76.481 ns int mult(类型化)72.581 ns短整数(对类型)90.772 ns字节短对(类型)90.772 ns字节对数87.859 ns字节多数(对类型)89.524 ns int [] trav 88.905 ns int [] trav(类型)89.126 ns short [] trav 10.563 ns short [] trav(类型)10.039 ns byte [] trav 8.356 ns byte [] trav(类型)8.338 ns我想有一个很多不必要的转换。这些测试在android标签上运行。
Bondax 2015年

5

如果您大量使用它们,则使用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个测试。

这是TestClass:

 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)

现在,我将int更改为TestClass中的字节,然后再次运行它。结果如下:

 Average Duration = 6.94375E8 ns = 694 ms (Approximately)

我相信这个实验表明,如果您要实例化大量变量,则使用字节代替int可以提高效率


4
请注意,此基准仅用于衡量与分配和构造相关的成本,并且仅用于具有多个单独字段的类的情况。如果对字段执行算术/更新操作,@ meriton的结果表明byte可能是>> slower << than than int
斯蒂芬·C

是的,我应该措辞更好地加以澄清。
WVrock

2

字节通常被认为是8位。short通常被认为是16位。

在“纯”环境中,这不是java,因为字节和多头,短裤和其他有趣事物的所有实现通常都对您隐藏,字节可以更好地利用空间。

但是,您的计算机可能不是8位,也可能不是16位。这意味着要特别获得16位或8位,将需要求助于浪费时间的“骗子”,以假装它具有在需要时访问这些类型的能力。

此时,这取决于硬件的实现方式。然而,从我一直坚强的观点来看,最好的速度是通过将事物存储在适合CPU使用的块中来实现的。64位处理器喜欢处理64位元素,而少于此的东西通常需要“工程魔术师”来假装它喜欢处理它们。


3
我不确定您所说的“工程魔术”是什么意思...大多数/所有现代处理器都有快速的指令来加载一个字节并对其进行符号扩展,从全角寄存器中存储一个字节并进行字节宽度处理或全角寄存器的一部分中的短角算术。如果您是对的,则在可行的情况下,用64位处理器上的long替换所有int是有意义的。
Ed Staub 2013年

我可以想象这是真的。我只记得在我们使用的Motorola 68k模拟器中,大多数操作可以使用16位值,而不能使用32位或64位。我以为这意味着系统具有可以最佳获取的首选值大小。尽管我可以想象现代的64位处理器可以同样轻松地获取8位,16位,32位和64位,但是在这种情况下,这不是问题。感谢您指出了这一点。
德米特里(Dmitry)

“ ...通常被认为是...” -实际上,明确地将>>指定<<明确地指定为这些大小。在Java中。这个问题的背景是Java。
斯蒂芬·C

大量处理器甚至使用相同数量的周期来操作和访问非字大小的数据,因此,除非您在特定的JVM和平台上进行测量,否则不必担心。
drrob

我想说的很笼统。那就是说,我实际上不确定Java在字节大小方面的标准,但是在这一点上,我非常确信,如果有任何异端决定非8位字节,Java不会用十英尺的杆触碰它们。但是,某些处理器需要多字节对齐,如果Java平台支持它们,它将需要做的事情变慢以适应处理这些较小的类型,或者用比您要求的更大的表示法神奇地表示它们。总是比其他类型更喜欢int,因为它总是使用系统喜欢的大小。
德米特里

2

短/字节/字符性能较差的原因之一是缺少对这些数据类型的直接支持。通过直接支持,这意味着JVM规范没有提及这些数据类型的任何指令集。诸如存储,加载,添加等指令具有用于int数据类型的版本。但是它们没有short / byte / char的版本。例如考虑下面的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虚拟机为int类型的数据提供了最直接的支持。这部分是因为期望Java虚拟机的操作数堆栈和局部变量数组的有效实现。它也受典型程序中int数据的出现频率的影响。其他整数类型的直接支持较少。例如,没有字节,字符或存储,加载或添加指令的简短版本。

引自此处的 JVM规范(页58)。


这些是反汇编的字节码;即JVM 虚拟指令。它们不是由javac编译器优化的,并且您不能从它们得出关于程序在现实生活中如何执行的任何可靠推断。JIT编译器将这些字节码编译为实际的本机机器指令,并在此过程中进行了一些相当认真的优化。如果要分析代码的性能,则需要检查本机代码指令。(这很复杂,因为您需要考虑多级x86_64管道的时序行为。)
Stephen C

我相信Java规范是供javac实现者实现的。因此,我认为在该级别上没有进行任何其他优化。无论如何,我也可能完全错了。请分享一些参考链接以支持您的陈述。
Manish Bansal

好吧,这是支持我的发言的一个事实。您不会找到任何(可信的)时序图来告诉您每个JVM字节码指令需要多少个时钟周期。当然不是由Oracle或其他JVM供应商发布的。另外,请阅读stackoverflow.com/questions/1397009
Stephen C

我确实找到了一份旧的(2008)论文,有人试图开发一个独立于平台的模型来预测字节码序列的性能。他们声称,与奔腾的RDTSC测量相比,他们的预测降低了25%。他们在禁用JIT编译的情况下运行JVM!参考:sciencedirect.com/science/article/pii/S1571066108004581
Stephen C,

我只是在这里感到困惑。我的回答是否支持您在复习部分中所述的事实?
Manish Bansal

0

区别几乎不明显!这更多是设计,适当性,统一性,习惯等问题。有时只是品味问题。当您关心的只是您的程序启动并运行并将a替换floatint不会损害正确性时,除非您可以证明使用任何一种类型都会改变性能,否则我认为这样做并没有好处。根据2或3个字节不同的类型进行性能调整实际上是您最后需要关心的事情;唐纳德·克努斯(Donald Knuth)曾经说过:“过早的优化是万恶之源”(不确定是他的邪恶,如果有答案,请编辑)。


5
Nit:A float 不能代表can的所有整数int;也不能int代表可以的任何非整数值float。也就是说,尽管所有int值都是long值的子集,但int 并不是 float的子集,而float 也不是int的子集。

我希望substituting a float for a double答题者打算写,如果是,答题者应编辑答案。如果不回答,则应出于@pst概述的原因以及许多其他原因而使羞愧的人退缩并回到基础知识。
高性能马克

@HighPerformanceMark不,我将int和float放进去是因为这就是我的想法。尽管我在考虑C,但我的答案并非特定于Java。刻薄的评论,你到了那里。
mrk
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.