在java中,使用byte或short而不是int和float而不是double是否更有效?

2022-08-31 09:58:14

我注意到我总是使用int和doubles,无论数字需要多小或多大。那么在java中,使用还是代替和代替更有效率?byteshortintfloatdouble

所以假设我有一个有很多int和double的程序。如果我知道这个数字适合,是否值得一试并将我的ints更改为字节或短路?

我知道java没有无符号类型,但是如果我知道这个数字只是正数,我能做些什么?

我所说的高效主要是指处理。我假设如果所有变量的大小都只有一半,那么垃圾回收器会快得多,并且计算也可能更快一些。(我想因为我在Android上工作,所以我也需要有点担心ram)

(我假设垃圾回收器只处理对象而不是基元,但仍然删除废弃对象中的所有基元,对吗?

我用一个小的Android应用程序尝试了它,但根本没有真正注意到差异。(虽然我没有“科学地”测量任何东西。

我是否错误地认为它应该更快,更有效率?我不想在一个大型程序中经历并改变一切,以发现我浪费了我的时间。

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


答案 1

我是否错误地认为它应该更快,更有效率?我不想在一个大型程序中经历并改变一切,以发现我浪费了我的时间。

简短的回答

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

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

更长的答案

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

有两个例外:

  • long和值需要 2 个基元 32 位单元格double
  • 基元类型的数组以打包形式表示,因此(例如)字节数组每32位字保存4个字节。

因此,可能值得优化使用和...和大型基元数组。但总的来说没有。longdouble

从理论上讲,JIT可能能够优化这一点,但在实践中,我从未听说过JIT可以。一个障碍是,在创建正在编译的类的实例之前,JIT 通常无法运行。如果JIT优化了内存布局,则可以拥有同一类对象的两个(或更多)“风格”......这将带来巨大的困难。


重新审视

从基准测试结果来看@meriton的答案,似乎使用而不是导致乘法的性能损失。实际上,如果孤立地考虑这些操作,则惩罚是巨大的。(你不应该孤立地考虑它们...但这是另一个话题。shortbyteint

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

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


次要问题

我知道java没有无符号类型,但是如果我知道这个数字只是正数,我能做些什么?

不。反正不是在性能方面。(在 、 、 等 中有一些方法用于将 、 等 处理为无符号。但这些并不能带来任何性能优势。这不是他们的目的。IntegerLongintlong

(我假设垃圾回收器只处理对象而不是基元,但仍然删除废弃对象中的所有基元,对吗?

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


答案 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;
        });
    }
}

在我的英特尔酷睿 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

如您所见,唯一显著的速度节省发生在遍历大型阵列时。使用较小的对象字段可以忽略不计,并且对于较小的数据类型,计算实际上略慢。

总体而言,性能差异非常小。优化算法远比选择基元类型重要。


推荐