在Java中,使用byte或short比使用int更有效率,而使用float比使用double更有效率吗?

111
我注意到我总是使用int和double,无论数字大小如何。那么在Java中,使用byte或short代替int,float代替double是否更有效率?
所以假设我有一个包含大量int和double的程序。如果我知道数字可以放入byte或short中,是否值得去更改我的int为byte或short?
我知道Java没有无符号类型,但如果我知道数字只会为正数,我是否可以额外进行某些操作?
通过“有效率”我主要指处理速度。我认为如果所有变量都是半尺寸,垃圾收集器可能会快得多,并且计算也可能更快。(我想因为我正在开发Android应用程序,所以我要担心RAM)
(我认为垃圾收集器只处理对象而不是基本类型,但仍会删除所有被遗弃的对象中的原始类型,对吗?)
我用我手头的一个小型android应用程序尝试过,但并没有真正注意到任何差异。(虽然我没有“科学地”测量任何内容。)
我是否错误地认为这样做应该更快,更有效率?我不想在一大堆程序中进行更改后发现我浪费了时间。
如果我开始一个新项目,是否值得这样做?(我的意思是每一点帮助似乎都会有好处,但另一方面,如果是这样的话,为什么似乎没有人这样做。)
7个回答

131

我是否错误地认为这样做会更快、更有效率?我不想浪费时间把一个大的程序中的所有内容都改掉,却发现白费劲了。

简短回答

是的,你错了。在大多数情况下,使用不同类型的数据类型相对于内存使用而言所带来的差异非常小。

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

详细回答

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

有两个例外:

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

因此,longdouble和大型原始类型数组的使用可能值得优化。但通常情况下不需要优化。

理论上,JIT 可能能够优化这一点,但在实践中我从未听说过可以这样做的JIT。其中一个障碍是JIT通常无法运行,直到已经创建了该类的编译实例。如果JIT优化了内存布局,你可以拥有同一类的两个或多个“版本”...那将会带来巨大的困难。


重新审视

查看 @meriton 回答中的基准测试结果,似乎使用 shortbyte 而不是 int 会导致乘法性能损失。事实上,如果您只考虑单个操作,这种损失是显著的。(您不应该只考虑它们……但这是另一个话题。)

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

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


次要问题

我知道java没有无符号类型,但如果我知道数字只会是正数,我还能做些什么额外的操作吗?

不。就性能而言没有任何额外的操作。(IntegerLong等中有一些用于处理 intlong等无符号类型的方法。但这些并不提供任何性能优势。这不是它们的目的。)

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

正确。对象的字段是对象的一部分,当对象被垃圾回收时,它将消失。同样,数组的单元格在数组被收集时也会消失。当字段或单元格类型为基本类型时,值存储在字段/单元格中,这是对象/数组的一部分,并且已被删除。


39
除非你有明确的性能问题证据,否则不要进行优化(代码等)。 - Bohemian
@meriton - 我非常确定对象布局在类加载时就已经确定了,并且在此之后不会再改变。请参见我的回答中的“细则”部分。如果实际内存布局在代码被JIT编译时发生变化,那么JVM将很难处理。(当我说JIT 可能优化布局时,这是假设和不切实际的...这可能解释了为什么我从未听说过JIT实际上这样做。) - Stephen C
我知道。我只是想指出,即使在创建对象后很难更改内存布局,JVM 仍然可以在此之前优化内存布局,即类加载时。换句话说,JVM 规范描述了带有单词偏移量的 JVM 行为,并不一定意味着必须实现 JVN 的方式 - 虽然大多数情况下都是这样。 - meriton
@meriton - JVM规范讨论了本地帧/对象中的“虚拟机字偏移量”。如何将它们映射到物理机器偏移量未指定。实际上,它无法指定...因为可能存在硬件特定的字段对齐要求。 - Stephen C
我注意到 shorts[i] = (short)(bytes[i] & 0xFF)shorts[i] = bytes[i] 快了约10%。我根据你的建议改成了 int[],但是 ints[i] = bytes[i] & 0xFF 仍然比 ints[i] = bytes[i] 快了约12%。有什么想法吗?这是否与符号扩展有关,应该在x86上用单个替换指令 MOVSX r32,r/m8 - Mark Jeronimus
显示剩余3条评论

33
这取决于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

正如您所看到的,只有在遍历大型数组时才会出现显著的速度提升;使用较小的对象字段几乎没有效益,并且在小数据类型上计算实际上略慢。

总体而言,性能差异非常小。优化算法比原始类型的选择更加重要。


3
与其说“尤其是在使用数组时”,我认为更简单的说法是,当存储足够大的数组时,shortbyte 的效率更高(数组越大,效率差异就越大;byte[2] 可能比 int[2] 更有效率,但差别不足以影响到结果),但是单个值作为 int 存储的效率更高。 - supercat
2
我所检查的是:这些基准测试总是使用int('3')作为因子或赋值操作数(循环变量,然后转换)。我的做法是根据lvalue类型使用有类型的因子/赋值操作数: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平板电脑上运行。 - Bondax

6

如果你在大量使用它们,使用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)

现在我正在将TestClass中的整数转换为字节,并再次运行它。以下是结果:

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

我相信这个实验表明,如果您正在实例化大量的变量,使用字节而不是整数可以提高效率。

6
请注意,这个基准测试仅测量与分配和构造相关的成本,仅适用于具有许多单独字段的类。如果在字段上执行算术/更新操作,则 @meriton 的结果表明 byte 可能比 int 更慢。 - Stephen C
是的,我应该用更好的措辞来澄清它。 - WVrock

2
翻译:short/byte/char 数据类型性能较差的原因之一是缺乏对这些数据类型的直接支持。所谓直接支持,指的是 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。由于这个原因,性能会降低。
现在,不直接支持short类型的原因如下所述:
Java虚拟机对int类型的数据提供了最直接的支持。这部分是为了预期Java虚拟机的操作数栈和局部变量数组的高效实现。这也受到典型程序中int数据频率的影响。其他整数类型的直接支持较少。例如,没有存储、加载或加法指令的byte、char或short版本。
引用自JVM规范此处(第58页)。

这些是反汇编的字节码,即JVM的虚拟指令。它们没有被javac编译器优化,您不能从中得出关于程序在实际运行中的表现的可靠推断。JIT编译器将这些字节码编译为实际的本机机器指令,并在此过程中进行了一些相当严格的优化。如果您想分析代码的性能,则需要检查本机代码指令。(而且这很复杂,因为您需要考虑多级x86_64管道的定时行为。) - Stephen C
我认为Java规范是供javac实现者实现的。因此,我不认为在那个层面上还有更多的优化。不过,我也可能完全错了。请分享一些参考链接来支持你的说法。 - Manish Bansal
这里有一个事实来支持我的说法。你不会找到任何可信的时间数据,告诉你每个JVM字节码指令需要多少时钟周期。当然,这些数据不是由Oracle或其他JVM供应商发布的。另外,请阅读https://stackoverflow.com/questions/1397009。 - Stephen C
我找到了一篇旧论文(2008年),其中有人试图开发一个平台无关的模型来预测字节码序列的性能。他们声称,与RDTSC测量相比,他们的预测偏差达到了25%...在Pentium上运行JVM时,他们禁用了JIT编译!参考:https://www.sciencedirect.com/science/article/pii/S1571066108004581 - Stephen C
我在这里有些困惑。我的回答难道不支持您在重新审视部分所陈述的事实吗? - Manish Bansal
1
不,它并不是。你的答案基于字节码做出了断言。正如我的评论所说,字节码不能让你推断性能,因此你的断言没有基于逻辑上合理的基础。现在,如果你转储本地代码并分析它们,并看到额外的本地指令来执行短<->长转换,那将是支持证据。但这不是。我们所知道的是,i2s 字节码指令可能会被 JIT 编译器优化掉。 - Stephen C

2

byte通常被认为是8位。 short通常被认为是16位。

在一个“纯净”的环境中,不是Java,因为所有字节、长整型、短整型和其他有趣的东西的实现通常都对你隐藏起来,byte更好地利用了空间。

然而,你的计算机可能不是8位,也可能不是16位。这意味着为了获取特定的16位或8位,它需要诉诸于“欺骗”,这浪费了时间,以便在需要时假装它具有访问这些类型的能力。

此时,这取决于硬件的实现方式。然而,从我所学到的知识来看,最好的速度是通过将东西存储在CPU舒适使用的块中来实现的。64位处理器喜欢处理64位元素,任何小于这个值的元素通常需要“工程魔法”来假装它喜欢处理它们。


3
我不确定您所说的 "engineering magic" 是什么意思……现代处理器中大多数/全部都有快速指令来加载字节并对其进行符号扩展,将完整宽度寄存器中的一个字节存储到一个位置,并在完整宽度寄存器的一部分进行字节宽度或短宽度算术运算。如果您是正确的,那么在64位处理器上,在可行的情况下,用longs替换所有ints会是有意义的。 - Ed Staub
我可以想象那是真的。我只记得在我们使用的Motorola 68k模拟器中,大多数操作都可以使用16位值,而不是32位或64位。我认为这意味着系统有一个首选的值大小,可以最优地获取它。虽然我可以想象现代的64位处理器可以同样容易地获取8位、16位、32位和64位,但在这种情况下,这不是问题。谢谢你指出这一点。 - Dmytro
“通常被认为是…” - 实际上,在Java中,它明确、明确地被指定为那些大小。而这个问题的背景也是Java。 - Stephen C
许多处理器甚至使用相同数量的周期来操作和访问非字大小的数据,因此除非您在特定的JVM和平台上进行测量,否则不值得担心。 - drrob
我试图以一般性的方式表达。话虽如此,我实际上不确定Java在字节大小方面的标准,但是现在我相信如果任何异端决定使用非8位字节,Java将不会碰它们。然而,一些处理器需要多字节对齐,如果Java平台支持它们,它将需要更慢地处理这些较小的类型,或者用比您请求的更大的表示来神奇地表示它们。始终优先选择int而不是其他类型,因为它始终使用系统喜欢的大小。 - Dmytro

1

我认为被接受的答案在说“在使用空间方面几乎没有区别”时有些错误。下面是一个例子,显示出差异在某些情况下非常不同:

Baseline usage 4.90MB, java: 11.0.12
Mem usage - bytes : +202.60 MB
Mem usage - shorts: +283.02 MB
Mem usage - ints  : +363.02 MB
Mem usage - bytes : +203.02 MB
Mem usage - shorts: +283.02 MB
Mem usage - ints  : +363.02 MB
Mem usage - bytes : +203.02 MB
Mem usage - shorts: +283.02 MB
Mem usage - ints  : +363.02 MB

验证代码:

static class Bytes {
    public byte f1;
    public byte f2;
    public byte f3;
    public byte f4;
}

static class Shorts {
    public short f1;
    public short f2;
    public short f3;
    public short f4;
}

static class Ints {
    public int f1;
    public int f2;
    public int f3;
    public int f4;
}

@Test
public void memUsageTest() throws Exception {
    int countOfItems = 10 * 1024 * 1024;
    float MB = 1024*1024;
    Runtime rt = Runtime.getRuntime();

    System.gc();
    Thread.sleep(1000);
    long baseLineUsage = rt.totalMemory() - rt.freeMemory();

    trace("Baseline usage %.2fMB, java: %s", (baseLineUsage / MB), System.getProperty("java.version"));

    for( int j = 0; j < 3; j++ ) {
        Bytes[] bytes = new Bytes[countOfItems];
        for( int i = 0; i < bytes.length; i++ ) {
            bytes[i] = new Bytes();
        }
        System.gc();
        Thread.sleep(1000);
        trace("Mem usage - bytes : +%.2f MB", (rt.totalMemory() - rt.freeMemory() - baseLineUsage) / MB);
        bytes = null;

        Shorts[] shorts = new Shorts[countOfItems];
        for( int i = 0; i < shorts.length; i++ ) {
            shorts[i] = new Shorts();
        }
        System.gc();
        Thread.sleep(1000);
        trace("Mem usage - shorts: +%.2f MB", (rt.totalMemory() - rt.freeMemory() - baseLineUsage) / MB);
        shorts = null;

        Ints[] ints = new Ints[countOfItems];
        for( int i = 0; i < ints.length; i++ ) {
            ints[i] = new Ints();
        }
        System.gc();
        Thread.sleep(1000);
        trace("Mem usage - ints  : +%.2f MB", (rt.totalMemory() - rt.freeMemory() - baseLineUsage) / MB);
        ints = null;
    }
}

private static void trace(String message, Object... args) {
    String line = String.format(US, message, args);
    System.out.println(line);
}

0

这种差异几乎不会被注意到!更多的是设计、适当性、统一性、习惯等问题...有时候只是品味的问题。当你关心的只是你的程序能否启动并且用float替换int不会影响正确性时,我认为除非你能证明使用任何一种类型都会改变性能,否则选择其中之一没有任何优势。基于在2或3个字节上不同的类型来调整性能真的是你最后应该关心的事情;Donald Knuth曾经说过:“过早地优化是万恶之源”(如果你知道答案,请编辑)。


5
注意:一个 float 类型无法表示所有整数,而一个 int 能够;同样地,一个 int 也无法表示任何 float 类型能够表示的非整数值。也就是说,虽然所有的 int 值都是 long 值的子集,但 int 不是 float 的子集,而 float 也不是 int 的子集。 - user166390
我认为回答者的意思是要写“用float替换double”,如果是这样,回答者应该编辑答案。如果不是,回答者应该感到羞愧,并且根据@pst和许多其他原因重新回到基础知识。 - High Performance Mark
@HighPerformanceMark 不是的,我使用int和float是因为那是我当时想到的。我的回答并不特定于Java,尽管我当时在想C语言...它是通用的。你的评论很刻薄。 - mrk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接