这两种不同代码解决方案的最佳性能是什么:
First letter upper-cased
VS
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;
}
}
,a
,b
我需要编写快速执行代码而不是内存优化。因此,我的性能基准是执行时间
答案 0 :(得分:6)
这很大程度上取决于你的内存访问模式。
第一个肯定更紧凑。
Java中的用户定义类型带来一些开销,类似于每个对象的指针开销(64位上的8个字节)。 Integer
可以占用16个字节(object
8个字节+ 4个字节,int
+ 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字节。
与此同时,每个包含一百ints
的3个数组只需要略多于1200个字节(对于存储长度和指针的数组开销,需要额外的一点点)。这非常接近第二版的一半大小。
顺序访问
当您按顺序处理数据时,速度和大小往往是相辅相成的。如果有更多数据可以放入页面和缓存行中,那么在机器指令在较大表示与较严格表示之间变化不大的情况下,代码通常会更快地消耗它。
因此,从顺序访问的角度来看,您需要一半内存的第一个版本可能会快得多(在某些情况下可能几乎快两倍)。
随机访问
然而,随机访问是另一种情况。让我们说a
,b
和c
同样是热门字段,总是在紧密循环中一起访问,这些循环在此结构中具有随机访问模式。
在这种情况下,你的第二个版本实际上可能会更好。这是因为它为MemeProp
对象提供了连续的布局,其中a
,b
和c
最终会在内存中彼此相邻,总是(无论垃圾收集器如何重新排列MemeProp
实例的内存布局。)
使用您的第一个版本,您的a
,b
和c
数组会在内存中展开。它们之间的步幅永远不会小于400字节。如果您在访问a[65]
,b[65]
和c[65]
时最终访问某个随机元素(例如第66个元素),这相当于可能会出现更多缓存未命中。如果这是我们第一次访问这些字段,我们最终会遇到3次缓存未命中。然后,我们可能会访问a[7]
,b[7]
和c[7]
,这些都会相对于a[65]
,b[65]
和{{1}相对228字节我们可能最终会再有3次缓存未命中。
可能比两者好
假设您需要随机AoS风格的访问,并且所有字段始终一起访问。在这种情况下,最佳表示可能是这样的:
c[65]
这最终会占用所有三种解决方案的最小内存量,并保证单个class Meme {
private int[] abc = new int[100 * 3];
}
的{{1}}字段彼此相邻。
当然,在某些情况下,您的里程可能会有所不同,但如果您需要随机和顺序访问,这可能是这三者中最强的候选者。
热/冷场分裂
为了完整起见,我们考虑一下您的内存访问模式是连续的但不是所有字段(abc
)一起访问的情况。相反,您有一个性能关键循环,它一起访问MemeProp
和a/b/c
,以及一些仅访问a
的非性能关键代码。在这种情况下,您可以从这样的表示中获得最佳结果:
b
这使得我们的数据看起来像这样:
c
......而不是:
class Meme {
private int[] ab = new int[100 * 2];
private int[] c = new int[100];
}
在这种情况下,通过提升abababab...
ccccc...
并将其放入单独的数组中,它不再与abcabcabcabc...
和c
字段交错,从而允许计算机进入"消耗"这些相关数据(a
和b
在这些性能关键循环中)的速度更快,因为它将此内存的连续块移动到更快但更小的内存形式(物理映射页面) ,CPU缓存行)。
SoA访问模式
最后,我们假设您分别访问所有字段。每个关键循环仅访问a
,仅访问b
,或仅访问a
。在这种情况下,您的第一个表示可能是最快的,特别是如果您的编译器可以发出有效的SIMD指令,可以并行处理多个这样的字段。
缓存行中的相关数据
如果您发现所有这些令人困惑,我不会责怪您,但有一些b
,一个计算机架构向导,在此网站上告诉过我。他以最优雅的方式总结了一切,目标应该是避免在缓存行中加载不相关的数据,而缓存行只会在不使用的情况下加载和驱逐。尽管我在所有的分析会话中都对此有了一些直觉,但我从未找到过如此简洁优雅的方式来理解所有这些缓存缺失。
我们的硬件和操作系统希望将内存从更大但更慢的内存形式转移到更小但更快的内存形式。当它这样做时,它倾向于通过少数人来抓住记忆。好吧,如果你试图从一个碗里抓住少数几个M& Ms,但你只对吃绿色的M& Ms感兴趣,那么抓住一把混合的M& Ms只会挑选出来绿色的,然后将所有其他的回到碗里。在这种情况下,如果你的碗里只装满了绿色的M& Ms,那就变得非常有效了。当你试图在高效的内存布局上解决时,如果我使用非常粗糙但是希望有用的类比。如果您想要在关键循环中访问所有类似的绿色M& Ms,请不要将它们(交错数据)与红色,蓝色,黄色等混合在一起。相反,保持所有那些绿色的权利在记忆中彼此相邻,这样当你用少数人抓住东西时,你只能得到你想要的东西。
面向数据的设计
如果您预计这些c
的大量输入循环方案正在设计您的外部公共界面,那么您正在做的事情之一是在harold
级别设置您的外部公共界面并转向{ {1}}字段为私人详细信息。
在您衡量之前,这可能是最有效的策略是识别批量处理数据的地方(尽管MemeProps
并非完全批量化,但我希望您的实际情况是更大的),并相应地设计您的公共接口。
例如,如果您正在设计Meme
类并且性能是关键目标,那么您希望避免公开MemeProp
对象,该对象在逐个像素的基础上提供操作。更好的是在批量100
或Image
级别设计此界面,允许批量处理一堆像素。
这使得您在测量和调整数据表示方面的余地远远超过了对一些粒度Pixel
对象接口具有一万个客户端依赖关系的设计,例如。
所以无论如何,最安全的选择是测量,但是你在接口设计的适当粗略水平上进行设计是件好事。