为什么我的Key中的'1'位越多,放在HashMap中的时间就越长?

时间:2012-02-16 05:18:53

标签: java performance hashmap bit sparse-matrix

我正在为一个类做一个项目,该类专注于在内存中存储一​​个大多数为0的值的巨大矩阵,并在其上执行一些矩阵数学运算。我的第一个想法是使用HashMap来存储矩阵元素,并且只存储非零的元素,以避免使用大量的内存。

我想为HashMap创建一个键,它代表元素的行号和列号,当我在地图中访问该条目时,我可以重新提取这两个值。我不知道Java和C# - 在C#中我会使用structRow成员Column,但在Java中我很快意识到没有用户值类型。在最后期限迫在眉睫的情况下,我安全地下注了Key一段时间。我使用一些非常简单的位移存储了前32位中的行数据(32位int)和最后32位中的列数据。 [编辑:我还要注意我的HashMap初始化时使用了一个特定的初始大小,它准确地表示了我存储在其中的值的数量,这是永远不会超过的。]

[旁注:我希望能够再次提取行/列数据的原因是大大提高矩阵乘法的效率,从O(n^2)O(n),以及更小的{{ 1}}开机]

在实现这个结构之后我注意到,从一个只给出非零元素的文本文件中读取一个23426 x 23426矩阵需要花费7秒钟,但它只需要2秒钟来计算特征值我们要求给予!在选择性地评论方法后,我得出结论,这7秒的大部分时间用于将我的值存储在n中。

HashMap

这是设置值的代码。如果我改用这种方法:

public void Set(double value, int row, int column) {
    //assemble the long key, placing row and column in adjacent sets of bits
    long key = (long)row << SIZE_BIT_MAX; //(SIZE_BIT_MAX is 32)
    key += column;
    elements.put(key, value);
}

阅读仅需2秒。这两个版本的密钥对于每个元素都是不同的,两者都是长类型,并且创建它们中的任何一个的实际代码的复杂性最小。它是public void Set(double value, int row, int column) { //create a distinct but smaller key (around 32 bits max) long key = (long)(row * matrixSize) + column; elements.put(key, value); } ,它在7秒和2之间产生差异。

我的问题是,为什么?我看到这些关键版本之间的区别在于第一个版本的位在整个和更频繁地设置为1,而第二个版本的所有最高32位设置为0.我是否追逐红鲱鱼,或者这是一个相当显着的差异在性能中elements.put(key, value)方法内部的结果?

3 个答案:

答案 0 :(得分:5)

看看Long如何实现hashCode()方法(至少在OpenJDK 7中):

public int hashCode() {
    return (int)(value ^ (value >>> 32));
}

这意味着您的密钥被填充回32位;所有低位都经常相互抵消,导致很多冲突,这需要HashMap花费额外的时间来寻找存储桶中的空闲插槽。你的第二种方法避免了这个问题,因为每个密钥生成的哈希码都是一个唯一值(因为你只有23426 x 23426 = 548777476项,它们很适合32位)。

因此,resaon是你的密钥选择,但不是设置位的数量。

但是,您对“用户值类型”究竟是什么意思?

public class MatrixKey {
    private final int row;
    private final int column;
    public MatrixKey(int row, int column) {
        this.row = row;
        this.column = column;
    }
    public int getRow() { return row; }
    public int getColumn() { return column; }
}

在实施MaphashCode()后,此类可以为Java中的equals()创建一个非常好的密钥。只需确保您不像hashCode那样实施Long方法。 :)

答案 1 :(得分:3)

JDK 6 documentation for Long.hashCode()开始(请注意,您的long原语被自动装箱到Long个对象 - 而在C#原语实际上对象):

  

返回此Long的哈希码。结果是此Long对象持有的原始long值的两半的异或。也就是说,哈希码是表达式的值:

(int)(this.longValue()^(this.longValue()>>>32))

我认为根据这个定义,这就解释了原因:

当你引入更多熵并因此通过long值的上半部分更多地分散它时,碰撞率会降低。编辑:我读过顺序错了,所以这是下面的反驳论点

当扩展到long范围时,冲突可能更有可能 - 毕竟,在Java中,hashCodes只有int大小,因此您只能进行有限数量的相等分配。如果你知道它在int范围内“均匀”分布,那么你的碰撞就会减少。如果你将其传播到long范围内,则会大大增加碰撞的可能性。

这是from the HashMap Java documentation(强调我的):

  

此实现为基本操作(get和put)提供了恒定时间性能,假设散列函数在桶之间正确地分散元素

附注:通过调整initial capacityload factor,您会发现更高的性能提升 - 请查看HashMap文档以获取更多信息。

答案 2 :(得分:1)

根据实施情况,您可能会遇到哈希冲突。

如果所有哈希值最终都在同一个“桶”中,则实现通常会将它们放到某个类型的列表中。如果是这种情况,您的访问时间将受到严重影响。