“structs”数组在Java中理论上可行吗?

“structs”数组在Java中理论上可行吗?,java,memory,jvm,memory-efficient,Java,Memory,Jvm,Memory Efficient,在某些情况下,需要一个高效的内存来存储大量对象。要在Java中实现这一点,您必须使用几个基本数组(请参见下文)或一个大字节数组,这会为转换带来一定的CPU开销 示例:您有一个类点{float x;float y;}。现在,您希望在一个数组中存储N个点,该数组在32位JVM上至少需要N*8字节的浮点值和N*4字节的引用值。因此,在正常的对象开销中,至少有1/3的垃圾没有被计算在内。但如果你将它存储在两个浮点数组中,一切都会好起来 我的问题:为什么Java不优化引用数组的内存使用?我的意思是为什么不

在某些情况下,需要一个高效的内存来存储大量对象。要在Java中实现这一点,您必须使用几个基本数组(请参见下文)或一个大字节数组,这会为转换带来一定的CPU开销

示例:您有一个类点{float x;float y;}。现在,您希望在一个数组中存储N个点,该数组在32位JVM上至少需要N*8字节的浮点值和N*4字节的引用值。因此,在正常的对象开销中,至少有1/3的垃圾没有被计算在内。但如果你将它存储在两个浮点数组中,一切都会好起来

我的问题:为什么Java不优化引用数组的内存使用?我的意思是为什么不直接把对象嵌入到数组中,就像C++中的那样? 例如,将类Point标记为final应该足以让JVM看到Point类的最大数据长度。或者这对谁不利?此外,在处理大型n维矩阵等时,这将节省大量内存

更新:

我想知道JVM在理论上是否可以优化它,例如在幕后以及在什么条件下——而不是我是否可以以某种方式强制JVM。我认为结论的第二点是,即使这样做也不容易

JVM需要知道的结论:

该类必须是final,以便JVM猜测一个数组项的长度 数组需要是只读的。当然,您可以更改点p=arr[i]这样的值;p、 setXi,但不能通过inlineArr[i]=新点写入数组。或者JVM必须引入复制语义,这将与Java方式背道而驰。见aroth的答案 如何初始化调用默认构造函数的数组或将成员初始化为其默认值
这不等于提供了下面这样的琐碎类吗

class Fixed {
   float hiddenArr[];
   Point pointArray(int position) {
      return new Point(hiddenArr[position*2], hiddenArr[position*2+1]);
   }
}

而且,在不让程序员明确声明他们想要它的情况下实现它也是可能的;JVM已经意识到C++中的值类型POD类型;其中只有其他普通的旧数据类型。我相信HotSpot在堆栈省略过程中使用了这些信息,没有理由它不能对数组也这样做吗?

这不等于提供了下面这样的简单类吗

class Fixed {
   float hiddenArr[];
   Point pointArray(int position) {
      return new Point(hiddenArr[position*2], hiddenArr[position*2+1]);
   }
}

而且,在不让程序员明确声明他们想要它的情况下实现它也是可能的;JVM已经意识到C++中的值类型POD类型;其中只有其他普通的旧数据类型。我相信HotSpot在堆栈省略过程中使用了这些信息,没有理由它不能对数组也这样做?

Java没有提供这样做的方法,因为它不是语言级别的选择。C、C++等类似的方法公开了这一点,因为它们是系统级编程语言,在这里,您希望了解系统级特征并根据所使用的特定架构做出决定。 在Java中,您的目标是JVM。JVM没有指定这是否是允许的,我假设这是真的;我还没有彻底梳理JLS来证明我就在这里。其思想是,当您编写Java代码时,您相信JIT能够做出智能决策。这就是引用类型可以折叠成数组或类似的地方。因此,这里的Java方法是,您不能指定它是否发生,但如果JIT可以进行优化并提高性能,那么它可以而且应该这样做

我不确定是否实现了这种优化,但我知道类似的优化是:例如,分配了new的对象在概念上位于堆上,但是,如果JVM通过一种称为escape analysis的技术注意到对象是本地方法,那么它可以在堆栈上甚至直接在CPU寄存器中分配对象的字段,从而完全消除堆分配开销,而不改变语言

更新问题的更新


如果问题是这样做是否可行,我想答案是肯定的。有一些特殊情况,例如空指针,但您应该能够解决它们。对于空引用,JVM可以说服自己永远不会有空元素,或者像前面提到的那样保留位向量。这两种技术可能都是基于转义分析的,转义分析显示数组引用永远不会离开方法,因为我可以看到,如果您尝试将其存储在对象字段中,簿记会变得很棘手。

Java没有提供这样做的方法,因为它不是语言级别的选择。C、C++等类似的方法公开了这一点,因为它们是系统级编程语言,在这里,您希望了解系统级特征并根据所使用的特定架构做出决定。 在Java中,您的目标是JVM。JVM没有指定这是否是允许的,我正在做一个假设 t这是真的;我还没有彻底梳理JLS来证明我就在这里。其思想是,当您编写Java代码时,您相信JIT能够做出智能决策。这就是引用类型可以折叠成数组或类似的地方。因此,这里的Java方法是,您不能指定它是否发生,但如果JIT可以进行优化并提高性能,那么它可以而且应该这样做

我不确定是否实现了这种优化,但我知道类似的优化是:例如,分配了new的对象在概念上位于堆上,但是,如果JVM通过一种称为escape analysis的技术注意到对象是本地方法,那么它可以在堆栈上甚至直接在CPU寄存器中分配对象的字段,从而完全消除堆分配开销,而不改变语言

更新问题的更新


如果问题是这样做是否可行,我想答案是肯定的。有一些特殊情况,例如空指针,但您应该能够解决它们。对于空引用,JVM可以说服自己永远不会有空元素,或者像前面提到的那样保留位向量。这两种技术都可能是基于转义分析的,转义分析显示数组引用从未离开过方法,因为我可以看到,如果您尝试将其存储在对象字段中,簿记会变得棘手。

您描述的场景可能会节省内存,但实际上我不确定它是否会这样做,但在实际将对象放入数组时,可能会增加相当多的计算开销。考虑当你做新的点时,你创建的对象在堆上动态分配。因此,如果通过调用new Point来分配100个点实例,则无法保证它们的位置在内存中是连续的,事实上,它们很可能不会被分配到连续的内存块

那么,一个点实例如何真正进入压缩数组呢?在我看来,Java必须显式地将点中的每个字段复制到为数组分配的连续内存块中。对于具有多个字段的对象类型来说,这可能会变得非常昂贵。不仅如此,原始点实例仍然占用堆上以及数组内部的空间。因此,除非它立即被垃圾回收,否则我认为任何引用都可以被重写,指向放置在数组中的副本,从而理论上允许对原始实例进行垃圾回收,实际上,如果您刚刚将引用存储在数组中,您将使用更多的存储空间

此外,如果您有多个压缩数组和一个可变对象类型,该怎么办?将对象插入数组必然会将该对象的字段复制到数组中。所以,如果你做了如下事情:

Point p = new Point(0, 0);
Point[] compressedA = {p};  //assuming 'p' is "optimally" stored as {0,0}
Point[] compressedB = {p};  //assuming 'p' is "optimally" stored as {0,0}

compressedA[0].setX(5)  
compressedB[0].setX(1)  

System.out.println(p.x);
System.out.println(compressedA[0].x);
System.out.println(compressedB[0].x);
…你会得到:

0
5
1

…即使从逻辑上讲,应该只有一个Point实例。存储引用可避免此类问题,也意味着在多个阵列之间共享一个非平凡对象的任何情况下,总存储使用量都可能低于每个阵列存储该对象所有字段的副本的情况

您描述的场景可能会节省内存,尽管实际上我不确定它是否会这样做,但在实际将对象放入数组时,它可能会增加相当多的计算开销。考虑当你做新的点时,你创建的对象在堆上动态分配。因此,如果通过调用new Point来分配100个点实例,则无法保证它们的位置在内存中是连续的,事实上,它们很可能不会被分配到连续的内存块

那么,一个点实例如何真正进入压缩数组呢?在我看来,Java必须显式地将点中的每个字段复制到为数组分配的连续内存块中。对于具有多个字段的对象类型来说,这可能会变得非常昂贵。不仅如此,原始点实例仍然占用堆上以及数组内部的空间。因此,除非它立即被垃圾回收,否则我认为任何引用都可以被重写,指向放置在数组中的副本,从而理论上允许对原始实例进行垃圾回收,实际上,如果您刚刚将引用存储在数组中,您将使用更多的存储空间

此外,如果您有多个压缩数组和一个可变对象类型,该怎么办?将对象插入数组必然会将该对象的字段复制到数组中。所以,如果你做了如下事情:

Point p = new Point(0, 0);
Point[] compressedA = {p};  //assuming 'p' is "optimally" stored as {0,0}
Point[] compressedB = {p};  //assuming 'p' is "optimally" stored as {0,0}

compressedA[0].setX(5)  
compressedB[0].setX(1)  

System.out.println(p.x);
System.out.println(compressedA[0].x);
System.out.println(compressedB[0].x);
…你会得到:

0
5
1
…即使从逻辑上讲,应该只有一个Point实例。存储引用可以避免此类问题,也意味着在任何情况下,如果要存储非平凡对象,都需要

如果在多个阵列之间共享,则您的总存储使用率可能低于每个阵列存储该对象所有字段的副本时的使用率

我看到的第一个问题是,您无法真正存储空引用…除非您在空引用与非空引用的位向量上花费另一块内存。hmmh有效问题。但是您可以为每个条目保留一位,或者将所有值初始化为默认值float get 0、boolean get false、references get null等,或者调用默认构造函数N次etcI发现了一种简单的Java方法:从带注释的对象生成代码!然后框架为该对象创建一个包装器类。看看这个例子和BattleshipsCell.java我看到的第一个问题是,你不能真正存储一个空引用…除非你在空引用和非空引用的位向量上花费另一块内存。但是您可以为每个条目保留一位,或者将所有值初始化为默认值float get 0、boolean get false、references get null等,或者调用默认构造函数N次etcI发现了一种简单的Java方法:从带注释的对象生成代码!然后框架为该对象创建一个包装器类。请参阅此示例和TraceSimsScel.javabutt,通过为每个数组访问创建新的点实例来考虑您不必要地翻动多少内存。CPU时间也一样,如果点构造函数碰巧做了一些不寻常的事情。新点将消耗更多空间。如果您迭代所有点或频繁使用它们,会怎么样?那你就没有好处了。值类型是原始类型吗?我确信JVM已经通过使用堆栈避免了这种分配,假设您立即使用返回的值。毕竟,这是它的工作。不过,我手头没有调试JVM来向您证明这一点。其他问题:您现在面临的问题是pointArray[I]!=pointArray[i],因为您每次都分配一个新对象。困难越来越大,提示JIT您希望对象内联是可能的,但我强烈怀疑Java作者宁愿自食其果,也不愿在==语言中添加额外的运算符重载,虽然我希望如此,但是考虑一下,通过为每个数组访问创建一个新的点实例,您不需要耗费多少内存。CPU时间也一样,如果点构造函数碰巧做了一些不寻常的事情。新点将消耗更多空间。如果您迭代所有点或频繁使用它们,会怎么样?那你就没有好处了。值类型是原始类型吗?我确信JVM已经通过使用堆栈避免了这种分配,假设您立即使用返回的值。毕竟,这是它的工作。不过,我手头没有调试JVM来向您证明这一点。其他问题:您现在面临的问题是pointArray[I]!=pointArray[i],因为您每次都分配一个新对象。困难越来越大。向JIT提示您希望对象内联是可能的,但我强烈怀疑Java作者宁愿自食其果,也不愿在==上添加额外的运算符重载,就像我希望的那样。嗯,如果理论上可能的话,我更希望在我认为JVM不可能进行这种优化的那一刻向docsAt提交一些要点?听起来像是。但我认为你是对的,这件事目前还没有完成。不,我认为这在理论上是不可能的。看看我的结论好吧,如果理论上可行的话,我更希望在我认为JVM的这种优化在理论上不可行的那一刻向docsAt提交一些要点?听起来像是。但我认为你是对的,这件事目前还没有完成。不,我认为这在理论上是不可能的。看一看我的结论,虽然在实践中我甚至不确定它是否会这样做,为什么?复制每个点的字段好,请重新阅读问题。我希望避免复制等。只使用数组中的原始字节来处理对象成员,就像在C++中所做的那样。您最后的结论是有意义的。这些是C++中的拷贝语义,不像java语言……如果数组被读取,则可以避免复制语义。only@Karussell-我不确定这是否有帮助,因为点对象通常是如何分配的。如果执行点[]x=新点[100],然后仅使用阵列中预打包的点实例,我可以看到总内存使用量减少。但一旦你开始做点p=新点0,0;x[0]=p;实际上,您使用了更多的空间,因为您有p8字节的存储空间,然后重复存储x[0]8字节,除非将p写入数组使p指向数组的副本并回收原始p。虽然在实践中我甚至不确定它是否会这样做,为什么?复制点中的每个字段好,请重新阅读问题。我会的
喜欢避免复制等。只需使用数组中的原始字节来处理对象成员,就像在C++中所做的那样,您的最后结论是有意义的。这些是C++中的拷贝语义,不像java语言……如果数组被读取,则可以避免复制语义。only@Karussell-我不确定这是否有帮助,因为点对象通常是如何分配的。如果执行点[]x=新点[100],然后仅使用阵列中预打包的点实例,我可以看到总内存使用量减少。但一旦你开始做点p=新点0,0;x[0]=p;实际上,您使用了更多的空间,因为您有p8字节的存储空间,然后重复存储x[0]8字节,除非将p写入数组使p指向数组的副本并回收原始p。