【问题标题】:Java Performance: multiple array vs single array of custom object [closed]Java性能:自定义对象的多个数组与单个数组[关闭]
【发布时间】:2016-04-01 21:15:03
【问题描述】:

这两种不同代码解决方案的最佳性能是什么:

class Meme {
  private int[] a = new int[100];
  private int[] b = new int[100];
  private int[] c = new int[100];
}

class Meme {
   private MemeProp[] prop = new MemeProp[100];
   class MemeProp {
     int a;
     int b;
     int c;
   }
}

考虑连续访问读写属性abc

我需要为快速执行而不是内存优化编写代码。因此,我的性能基准是执行时间

【问题讨论】:

  • 您可以编写几个测试并自己计时...
  • 几乎任何时候你试图猜测编译器将如何优化你都会出错的事情。对于像 Java 这样的 JIT 编译语言来说,尤其是如此。
  • 第一个几乎肯定会更快。即使在像 C++ 这样带有未装箱对象的低级语言中也是如此;盒装的会更重要。
  • 另外,如果您非常在意速度,请使用 C。
  • 这个基于意见的是什么世界?

标签: java arrays performance


【解决方案1】:

这在很大程度上取决于您的内存访问模式。

第一个肯定更紧凑。

Java 中的用户定义类型会带来一些开销,例如每个对象的指针开销(64 位上的 8 个字节)。例如,Integer 可以占用 16 个字节(object 为 8 个字节,int 为 4 个字节,对齐为 4 个字节),而int 仅占用 4 个字节。它类似于class,C++ 中的虚函数存储vptr

鉴于此,如果我们查看MemeProp 的内存使用情况,我们会得到这样的结果:

  class MemeProp {
     // invisible 8 byte pointer with 8-byte alignment requirements
     int a; // 4 bytes
     int b; // 4 bytes
     int c; // 4 bytes
     // 4 bytes of padding for alignment of invisible field
   }

生成的内存大小为每个 MemeProp 实例 24 字节。当我们取其中的一百个时,最终总内存使用量为 2400 字节。

同时,您的 3 个数组每个包含 100 个ints 只需要略多于 1200 个字节(存储长度和指针的数组开销需要一点点额外)。这非常接近第二个版本的一半。

顺序访问

当您按顺序处理数据时,速度和大小往往是齐头并进的。如果一个页面和缓存行可以容纳更多数据,那么在机器指令在较大表示和较紧表示之间变化不大的情况下,您的代码通常会更快地消耗它。

因此,从顺序访问的角度来看,需要一半内存的第一个版本可能会快很多(在某些情况下可能快两倍)。

随机访问

然而,随机访问是另一种情况。假设abc 是同样热门的字段,总是在紧密循环中一起访问,这些循环对这个结构具有随机访问模式。

在这种情况下,您的第二个版本实际上可能会更好。这是因为它为MemeProp 对象提供了一个连续的布局,其中abc 最终会在内存中彼此相邻(无论垃圾收集器最终如何重新排列MemeProp 实例的内存布局)。

在您的第一个版本中,您的 abc 数组分布在内存中。它们之间的步幅永远不能小于 400 字节。当我们访问a[65]b[65]c[65] 时,如果最终访问某个随机元素(例如第 66 个元素),这等同于潜在的更多缓存未命中。如果这是我们第一次访问这些字段,我们最终会出现 3 次缓存未命中。然后我们可能会访问a[7]b[7]c[7],这些相对于a[65]b[65]b[65]c[65] 相距228 个字节,我们最终可能会多出3 个缓存未命中.

可能比两者都好

假设您需要随机 AoS 样式的访问,并且始终一起访问所有字段。在这种情况下,最优化的表示可能是这样的:

class Meme {
    private int[] abc = new int[100 * 3];
}

这最终占用了所有三个解决方案中最少的内存量,并保证单个 MemePropabc 字段彼此相邻。

当然,在某些情况下您的里程可能会有所不同,但如果您需要随机访问和顺序访问,这可能是这三者中最强的候选者。

热/冷场分离

为了完整起见,让我们考虑一种情况,即您的内存访问模式是顺序的,但并非所有字段 (a/b/c) 都被一起访问。相反,您有一个性能关键循环,它同时访问ab,以及一些仅访问c 的非性能关键代码。在这种情况下,您可能会从这样的表示中获得最佳结果:

class Meme {
    private int[] ab = new int[100 * 2];
    private int[] c = new int[100];
}

这使得我们的数据看起来像这样:

abababab...
ccccc...

...与此相反:

abcabcabcabc...

在这种情况下,通过吊出c并将其放入单独的数组中,它不再与ab字段交错,允许计算机“消费”这些相关数据(ab 在那些性能关键的循环中)以更快的速度移动此内存的连续块到更快但更小的内存形式(物理映射页面,CPU 缓存行)。

SoA 访问模式

最后,假设您要分别访问所有字段。每个关键循环只访问a,只访问b,或者只访问c。在这种情况下,您的第一个表示可能是最快的,尤其是如果您的编译器可以发出高效的 SIMD 指令,这些指令可以并行处理多个此类字段。

缓存行中的相关数据

如果您发现所有这些令人困惑,我不怪您,但是计算机体系结构向导 harold 曾经在此站点上告诉过我一些事情。他以最优雅的方式总结了这一切,目标应该是避免在缓存行中加载不相关数据,该缓存行只会被加载和驱逐而不被使用。尽管我在所有的分析会话中都对此有了一些直觉,但我从来没有找到一种简洁而优雅的方式来表达所有这些缓存未命中的情况。

我们的硬件和操作系统希望将内存从较大但较慢的内存形式转移到较小但较快的内存形式。当它这样做时,它往往会“抓住少数人的记忆”。好吧,如果你想从碗里抓一把 M&M,但你只对吃绿色的 M&M 感兴趣,那么抓一把混合的 M&M 只挑出绿色的,然后把其他的都还回去,效率会很低。那些到碗里。在这种情况下,如果你有一碗只装满绿色 M&M 巧克力豆,它会变得更加高效,如果我使用一个非常粗略但希望有帮助的类比,那么当你试图确定一个有效的内存布局时,这就是你的目标。如果您想在关键循环中访问的只是类比绿色 M&M,请不要将它们与红色、蓝色、黄色等混合(交错数据)。而是让所有绿色 M&M 彼此相邻在内存中,这样当你一把抓起东西时,你只会得到你想要的。

面向数据的设计

如果您预计这些 MemeProps 会出现大量输入、循环场景,那么您正在做的事情之一是在集合级别、Meme 级别设计您的外部公共接口,并将 MemeProp 字段转换为一个私人细节。

这可能是您衡量之前最有效的策略,即确定您批量处理数据的位置(尽管 100 并不完全是批量,我希望您的实际场景更大),并设计您的公共界面相应地。

例如,如果您正在设计一个Image 类并且性能是一个关键目标,那么您希望避免暴露一个提供逐像素操作的Pixel 对象。更好的是在批量ImageScanline 级别设计此接口,允许批量处理一堆像素。

这为您提供了更多的回旋余地来测量和调整数据表示,而不是具有一万个客户端依赖于一些表示单个像素的细粒度 Pixel 对象接口的设计,例如

因此,无论如何,最安全的选择是测量,但最好在界面设计的适当粗略水平上进行设计。

【讨论】:

  • "如果abc 总是一起访问,那么顺序访问甚至比你的第一个版本更好。" → 这令人惊讶地不是真的(至少在 C++ 中),因为预取器似乎在并行流上工作得更好。 See "Compact AOS vs SOA" on this slideshow. 不过,关于随机访问的评论是有效的,尽管只是在很小的程度上,而不是在这些数组长度上。 (对冷缓存的随机访问是另一回事。)
  • @Veedrac 啊,是的,尽管如果内存访问模式类似于 AoS,我从未见过编译器实际上针对 SoA 风格的 SIMD 内在函数进行了优化。也就是说,如果每个单独的组件实际上有三个循环,我希望a/b/c 并行数组版本更快(a/b/c 不是在一个循环中一起访问,而是在三个单独的循环中单独访问以进行垂直矢量化)。不过,我会尝试更新答案,以包括 SoA 优化(但通常我认为您必须拥有垂直而不是水平的访问模式才能从中受益)。
  • 在您的“可能比两者都好”的情况下,计算索引值所需的额外时间怎么样,即,而不是 a[i]、b[i]、c[i],而不是你需要 abc[3*i]、abc[3*i+1]、abc[3*i+2]?这不会慢很多吗?
  • @WillKanga 这变得非常复杂和特定于架构,但整数乘法往往非常快(与往往非常慢的除法相比),并且顺序访问模式通常会转化为加法而不是乘法指示。这只是粗略的建议,但绝不会优先考虑算术成本而不是内存访问。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-09
  • 1970-01-01
相关资源
最近更新 更多