【问题标题】:Tweaking java classes for CPU cache friendliness为 CPU 缓存友好性调整 Java 类
【发布时间】:2014-06-23 13:08:15
【问题描述】:

在设计 java 类时,对于实现 CPU 缓存友好性有哪些建议?

到目前为止,我学到的是应该尽可能多地使用 POD(即 int 而不是 integer)。因此,在分配包含对象时,数据将被连续分配。例如

class Local
{
    private int data0;
    private int data1;
    // ...
};

比缓存更友好

class NoSoLocal
{
    private Integer data0;
    private Integer data1;
    //...
};

后者将需要对 Integer 对象进行两次单独的分配,这些对象可以位于内存中的任意位置,尤其是。 GC 运行后。 OTOH 第一种方法可能会在数据可以重复使用的情况下导致数据重复。

有没有办法让它们在内存中彼此靠近,这样父对象及其包含的元素就会立即在 CPU 缓存中,而不是任意分布在整个内存中,而且 GC 会将它们保持在一起?

【问题讨论】:

  • 您想保持任意引用类型实例“在内存中彼此靠近”还是专门针对盒装 POD?
  • 更多的是关于保持相关对象,即所有元素(不仅是它们的引用)在内存中彼此靠近。
  • 如果你真的担心这个,Java 不适合你
  • 所以我做了一些基准测试:在单线程场景中,差异在 1.2 和 1.9 之间,有利于本地数据。在多线程场景下,本地数据的优势在9-20倍之间。因此,在服务器端代码的数据局部性上花费一些精力肯定是值得的……
  • 在我的 i7 4770 上测试了相同的代码(其他测试是在我的 i5 笔记本电脑上):这里在 MT 场景中本地数据的处理速度比使用引用的数据快 35 倍。

标签: java caching garbage-collection jvm


【解决方案1】:

您不能强制 JVM 将相关对象彼此靠近放置(尽管 JVM 会尝试自动执行此操作)。但是有一些技巧可以让 Java 程序对缓存更加友好。

让我向您展示一些现实项目中的例子。

注意!不推荐使用 Java 编码方式!
除非您完全确定为什么要这样做,否则不要采用以下技术。

  1. 继承优于组合。你肯定听说过相反的原则"Favor composition over inheritance"。但是对于作文,您有额外的参考要遵循。这对缓存局部性不利,并且还需要更多内存。 inheritance over composition 的经典示例是 JDK 8 Adder 和 Accumulator 类,它们扩展了实用程序 Striped64 类。

  2. 将结构数组转换为数组结构。 这再次有助于节省内存并加快单个字段的批量操作,例如键查找:

    class Entry {
        long key;
        Object value;
    }
    
    Entry[] entries;
    

    将被替换为

    long[] keys;
    Object[] values;
    
  3. 通过内联扁平化数据结构。 我最喜欢的示例是内联 160 位 SHA1 哈希,由 byte[] 表示。之前的代码:

    class Blob {
        long offset;
        int length;
        byte[] sha1_hash;
    }
    

    后面的代码:

    class Blob {
        long offset;
        int length;
        int hash0, hash1, hash2, hash3, hash4;
    }
    
  4. 替换 String char[]。你知道,Java 中的 String 在底层包含 char[] 对象。为什么要为额外的参考付出绩效惩罚?

  5. 避免使用链表。链表对缓存非常不友好。硬件最适合线性结构。 LinkedList 通常可以替换为 ArrayList。经典的HashMap 可以替换为open address hash table

  6. 使用原始集合。Trove 是一个高性能库,包含原始类型的专用列表、映射、集合等。

  7. 在数组或 ByteBuffers 之上构建您自己的数据布局。 字节数组是完美的线性结构。为了获得最佳的缓存局部性,您可以手动将对象数据打包到一个单字节数组中。

【讨论】:

    【解决方案2】:

    在可以重复使用数据的情况下,第一种方法可能会导致数据重复。

    但不是你提到的情况。 int 是 4 个字节,引用通常是 4 个字节,因此使用整数不会获得任何收益。但是,对于更复杂的类型,它可能会产生很大的不同。

    有没有办法让它们在内存中彼此靠近,这样父对象及其包含的元素就会立即在 CPU 缓存中,而不是任意分布在整个内存中,而且 GC 会将它们保持在一起?

    只要对象只在一个地方使用,GC 无论如何都会这样做。如果对象在多个地方使用,它们将接近一个引用。

    注意:不能保证一定是这样,但是在分配对象时,它们通常在内存中是连续的,因为这是最简单的分配策略。复制保留对象时,HotSpot GC 会按照发现的相反顺序复制它们。即它们仍然在一起,但顺序相反。

    注意 2:使用 4 个字节作为 int 仍然比使用 28 个字节作为整数更有效(4 个字节作为引用,16 个字节作为对象头,4 个字节作为值,4 个字节作为填充)

    注意 3:最重要的是,您应该优先考虑清晰度而不是性能,除非您已经衡量了自己的需要并拥有更高效的解决方案。在这种情况下,int 不能为空,但integer 可以为空。如果你想要一个不应该是null 的值,请使用int,不是为了性能,而是为了清晰。

    【讨论】:

    • But not in the case you mention. 我知道,这只是作为示例。在我目前的情况下,它是一个更复杂的对象结构,其中元素将逐个填充。重要的一点是:是否可以保证仅由另一个对象拥有的对象将始终位于拥有对象的附近
    • @ogni42 不能保证,如果对象被多次使用是不可能的。但是,如果对象被引用一次,那么如果它们是由程序创建或同时由 GC 复制的,则它们很可能在一起。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-03-22
    • 1970-01-01
    • 2014-06-21
    • 2020-11-20
    • 2011-09-17
    • 1970-01-01
    • 2017-04-11
    相关资源
    最近更新 更多