【问题标题】:Slow initialization of large array of small objects大型小对象数组的缓慢初始化
【发布时间】:2015-12-01 21:21:55
【问题描述】:

我今天偶然发现了这个案例,我想知道这个巨大的时间差异背后的原因是什么。

第一个版本初始化一个 5k x 5k 的原始整数数组:

public void initializeRaw() {
  int size = 5000;
  int[][] a = new int[size][size];
  for (int i = 0; i < size; i++)
    for (int j = 0; j < size; j++)
      a[i][j] = -1;
}

在我的机器上大约需要 300 毫秒。 另一方面,用简单的 2-int 结构初始化同一个数组:

public class Struct { public int x; public int y; }

public void initializeStruct() {
  int size = 5000;
  Struct[][] a = new Struct[size][size];
  for (int i = 0; i < size; i++)
    for (int j = 0; j < size; j++)
      a[i][j] = new Struct();
}

占用超过 15000 毫秒。

我希望它会慢一点,毕竟要分配的内存更多(如果我没记错的话,是 10 个字节而不是 4 个字节),但我不明白为什么要花 50 倍的时间。

谁能解释一下?也许有更好的方法来在 Java 中进行这种初始化?

编辑:为了进行一些比较 - 使用 Integer 而不是 int/Struct 的相同代码的工作时间为 700 毫秒 - 只慢了两倍。

【问题讨论】:

  • 在后面的代码中,Struct的构造函数被调用了(size)^2次,原因之一……
  • 我很想看看 Integer 的性能,可能会指出为什么在所有条件相同的情况下首选基元。
  • 您在欺骗自己,称您的类为结构。它仍然是一个类,并且随之而来的是所有的初始化开销。
  • -verbose:gc会解释很多
  • 是的,它带来了开销,但为什么这么大?如果我理解正确,使用 Integer 应该会产生类似的开销,但实际上它只有 ~700ms - 比 50 慢 2 倍。

标签: java arrays performance


【解决方案1】:

我希望它会慢一点,毕竟要分配的内存更多(如果我没记错的话,是 10 个字节而不是 4 个字节),但我不明白为什么要花 50 倍的时间。

不,比这更糟糕。在第一种情况下,您正在创建 5001 个对象。在第二种情况下,您将创建 25,005,001 个对象。我怀疑每个Struct 对象将占用16 到32 个字节。 (这将取决于各种 JVM 细节,但这是一个粗略的猜测。)

在第一种情况下,您的 5001 个对象总共需要大约 100MB。如果您在一个具有 64 位引用的平台上,等效对象(数组)可能总共需要 ~200MB...那么还有另外 2500 万个对象需要分配和初始化。

所以是的,一个相当大的差异......

【讨论】:

  • "gigabytes" 在这里可能有点太多了。 16 字节(假设单一类型指针和两个整数)加起来是 400 MB,加到 200MB 的指针应该是 600MB。它确实是内存的 6 倍,但绝对不是 50。构造函数的调用确实更多,但实际上应该归结为分配和修改 600MB。有没有办法有效地解决这个问题?必须将结构拆分为两个较小的数组,从而混淆代码以获得整个数量级的改进,这对我来说似乎是一个不可能的要求。
  • @Piotr:你在数学上是对的。不知道我做的时候发生了什么。但我看不出必须在清晰度和性能之间找到平衡有什么“不可能”的。这些人经常处于紧张状态。
【解决方案2】:

当您创建一个包含 5000 个整数的数组时,您将一次性为所有这些整数分配所需的所有空间,作为一个连续元素块。当您为每个数组元素分配一个 int 时,您并没有分配任何东西。将其与 5000 个 Struct 实例的数组进行对比。您遍历该数组,并为这 5000 个元素中的每一个元素分配一个 Struct 实例。分配一个对象比简单地将一个 int 值写入一个变量需要更长的时间。

您拥有二维数组这一事实在这里并没有太大的比较差异,因为这只是意味着您在两种情况下都分配了 5000 个数组对象。

如果您正在对 Integer 对象数组进行计时,然后将每个元素设置为 -1,那么您不会每次都分配单独的 Integer 对象。相反,您使用的是自动装箱,这意味着编译器隐式调用 Integer.valueOf(-1) 并且该方法每次都从缓存中返回相同的对象。

更新:回到解决您的问题,如果我理解正确,您需要将 5000x5000 结构保留在 2D 数组中,并且您对创建此数组所需的时间比使用原语长得多感到失望。为了提高性能,您可以创建两个基元数组,一个用于 Struct 的每个字段,但这会降低代码的清晰度。

您还可以创建一个 long 数组(因为每个 long 是 int 大小的两倍)并使用 & 和 >> 运算符来获取您的原始 int。同样,这会降低代码的清晰度,但您将只有一个数组。

但是,您似乎只专注于代码的一个部分,即数组的创建。您可能会发现您对每个元素所做的处理掩盖了创建数组所需的时间。分析您的整个应用程序并查看数组的创建是否重要。

【讨论】:

  • 感谢您的详尽回答。分配确实占用了大部分处理时间 - 需要 15 秒,整个数据处理大约需要 0.2 秒 - 这就是我注意到它的原因。我从来没有用 java 编码,但是在看到我只是缺少一些特定于 java 的解决方案之后我最感兴趣 - 我很惊讶没有任何解决方案。我想从这里的一个简单的结论是你不应该过度使用小数据结构(当它们有很多时)来清理代码,因为它非常昂贵。
  • 一个有趣的转移是,如果您使用 C#,您可以使用结构而不是类,然后它不会导致分配这么多对象,因为结构确实是一部分数组本身。仅仅因为这个原因,改变编程语言有点激烈。
  • 这是否意味着自动装箱在使用 -1 设置 Integer 对象时不起作用或已优化?我认为所有原始包装器都在引擎盖下自动装箱。
  • 自动装箱仍在发挥作用,但它使用现有对象自动装箱原始值,而不是创建新对象。这通常发生在较小的 int 值上,并且取决于实现。例如,Oracle 的 Java 8 实现缓存了值从 -128 到 127 的整数。(但你永远不应该依赖它,例如,通过在 Integer 上使用 == 运算符。)
猜你喜欢
  • 2017-08-03
  • 2012-10-31
  • 1970-01-01
  • 1970-01-01
  • 2018-08-25
  • 1970-01-01
  • 2021-07-19
  • 2018-06-22
  • 1970-01-01
相关资源
最近更新 更多