繁体   English   中英

我应该如何以内存效率的方式将字符串键映射到Java中的值?

[英]How should I map string keys to values in Java in a memory-efficient way?

我正在寻找一种存储字符串 - > int映射的方法。 当然,HashMap是一个最明显的解决方案,但由于我受内存限制,需要存储200万对,7个字符长的密钥,我需要一些内存有效的东西,检索速度是次要参数。

目前我正沿着以下方向前进:

List<Tuple<String, int>> list = new ArrayList<Tuple<String, int>>();
list.add(...); // load from file
Collections.sort(list);

然后进行检索:

Collections.binarySearch(list, key); // log(n), acceptable

我是否可以选择自定义树(每个节点都是一个字符,每个叶子都有结果),或者是否有适合这种情况的现有集合? 这些字符串实际上是顺序的(英国邮政编码,它们没有多大区别),所以我期待在这里节省大量内存。

编辑 :我刚刚看到你提到字符串是英国邮政编码,所以我相当自信你使用Trove TLongIntHashMap不会出错:顺便说一下, Trove是一个小型库,它非常容易使用。

编辑2 :很多人似乎觉得这个答案很有趣,所以我正在添加一些信息。

这里的目标是以一种以内存效率的方式使用包含键/值的映射,因此我们将首先查找内存有效的集合。

以下SO问题是相关的(但与此相同)。

什么是最有效的Java Collections库?

Jon Skeet提到Trove “只是一个来自原始类型的集合库” [原文如此],实际上,它并没有增加太多功能。 我们还可以看到一些关于Trove的内存和速度与默认集合相比的基准(由.duckman提供 )。 这是一个片段:

                      100000 put operations      100000 contains operations 
java collections             1938 ms                        203 ms
trove                         234 ms                        125 ms
pcj                           516 ms                         94 ms

还有一个示例显示使用Trove而不是常规Java HashMap可以节省多少内存:

java collections        oscillates between 6644536 and 7168840 bytes
trove                                      1853296 bytes
pcj                                        1866112 bytes

因此,尽管基准测试总是需要花费一些时间,但很明显, Trove不仅会节省内存,而且会更快。

因此,我们现在的目标是使用Trove(通过在常规HashMap中投入数百万条条目,您的应用开始感到反应迟钝)。

你提到了200万对,7个字符的长键和一个String / int映射。

2000000是真的不那么多,但你还是会觉得“对象”开销和原语的常数(UN)拳击整数在一个普通的HashMap {字符串,整数}这就是为什么特罗韦使得有很大的意义在这里。

但是,我要指出,如果你可以控制“7个字符”,你可以更进一步:如果你只使用ASCII或ISO-8859-1字符,那么你的7个字符就会很长( *)。 在这种情况下,您可以完全躲避对象创建,并在很长时间内代表您的7个角色。 然后,您将使用Trove TLongIntHashMap并完全绕过“Java对象”开销。

你明确指出你的密​​钥是7个字符长然后评论他们是英国邮政编码:我将每个邮政编码映射到一个长的,并通过使用Trove将数百万个键/值对装入内存来节省大量内存。

Trove的优势基本上在于它不会对对象/原语进行持续的装箱/拆箱:在很多情况下,Trove只能直接使用基元和基元。

String键编码为long的示例方法(假设ASCII字符,每个字符一个字节用于简化 - 7位就足够了):

long encode(final String key) {
    final int length = key.length();
    if (length > 8) {
        throw new IndexOutOfBoundsException(
                "key is longer than 8 characters");
    }
    long result = 0;
    for (int i = 0; i < length; i++) {
        result += ((long) ((byte) key.charAt(i))) << i * 8;
    }
    return result;
}

使用Trove库。

Trove库已经为基元优化了HashMapHashSet类。 在这种情况下, TObjectIntHashMap<String>会将参数化对象( String )映射到基本int

首先,您是否测量到LinkedList确实比HashMap更具内存效率,或者您是如何得出这个结论的? 其次, LinkedList的元素访问时间为O(n) ,因此您无法对其进行有效的二进制搜索。 如果你想做这样的方法,你应该使用一个ArrayList ,它可以让你在性能和空间之间做出妥协。 然而,我再次怀疑HashMapHashTable或者 - 特别是 - TreeMap将消耗更多的内存,但前两个将提供常量访问和树映射对数,并提供一个比普通列表更好的接口。 我会尝试做一些测量,内存消耗的差异究竟是多少。

更新 :正如Adamski指出的那样, String本身,而不是它们存储的数据结构,将消耗最多的内存,查看特定于字符串的数据结构可能是个好主意,例如尝试 (特别是patricia尝试 ),这可能会减少字符串所需的存储空间。

你正在寻找的是一个简洁的特里 - 一个trie ,它在理论上可以将其数据存储在几乎最小的空间内。

不幸的是,目前没有适用于Java的简洁类库。 我的下一个项目之一(在几周内)就是为Java (和其他语言)编写一个。

同时,如果你不介意JNI ,你可以参考几个 很好的本地简洁图书馆。

你看过尝试了吗? 我没有使用它们,但它们可能适合你正在做的事情。

自定义树将具有与O(log n)相同的复杂性,请勿打扰。 你的解决方案是合理的,但我会使用ArrayList而不是LinkedList因为链表每个存储值分配一个额外的对象,这相当于你的案例中的很多对象。

正如Erick所写,使用Trove库是一个很好的起点,因为你在存储int原语而不是Integer s中节省了空间。

但是,您仍然面临存储200万个String实例的问题。 鉴于这些是地图中的关键,实习他们不会提供任何好处,所以接下来我要考虑的是是否有一些可以被利用的字符串的特征。 例如:

  • 如果String表示常用单词的句子,那么您可以将String转换为Sentence类,并实习单个单词。
  • 如果字符串仅包含Unicode字符的子集(例如,仅字母AZ或字母+数字),则可以使用比Java的Unicode更紧凑的编码方案。
  • 您可以考虑将每个String转换为UTF-8编码的字节数组,并将其包装在类: MyString 显然,这里的权衡是执行查找所花费的额外时间。
  • 您可以将地图写入文件,然后将内存映射到文件的一部分或全部。
  • 您可以考虑使用诸如Berkeley DB之类的库来定义持久映射并在内存中缓存一部分映射。 这提供了可扩展的方法。

也许你可以使用RadixTree

使用java.util.TreeMap而不是java.util.HashMap 它使用红黑二进制搜索树,并且不使用比保存包含地图中元素的注释所需的更多内存。 没有额外的桶,不像HashMap或Hashtable。

我认为解决方案是在Java之外做一点。 如果您有这么多值,则应使用数据库。 如果您不想安装Oracle,SQLite快速而简单。 这样,您不需要的数据就会存储在磁盘上,所有的缓存/存储都会为您完成。 设置具有一个表和两列的DB不会花费太多时间。

我考虑使用一些缓存,因为它们通常具有溢出到磁盘的能力。

问题是对象的内存开销,但使用一些技巧可以尝试实现自己的hashset。 这样的东西。 像其他人一样,字符串的开销很大,所以你需要以某种方式“压缩”它。 另外,尽量不要在哈希表中使用太多的数组(列表)(如果你做链接类型哈希表),因为它们也是对象,也有开销。 更好的是开放寻址哈希表。

您可以创建符合您需求的密钥类。 也许是这样的:

public class MyKey implements Comparable<MyKey>
{
    char[7] keyValue;

    public MyKey(String keyValue)
    {
        ... load this.keyValue from the String keyValue.
    }

    public int compareTo(MyKey rhs)
    {
        ... blah
    }

    public boolean equals(Object rhs)
    {
        ... blah
    }

    public int hashCode()
    {
        ... blah
    }
}

试试这个

OptimizedHashMap<String, int[]> myMap = new OptimizedHashMap<String, int[]>();
for(int i = 0; i < 2000000; i++)
{
  myMap.put("iiiiii" + i, new int[]{i});
}
System.out.println(myMap.containsValue(new int[]{3}));
System.out.println(myMap.get("iiiiii" + 1));

public class OptimizedHashMap<K,V> extends HashMap<K,V>
{
    public boolean containsValue(Object value) {
    if(value != null)
    {
        Class<? extends Object> aClass = value.getClass();
        if(aClass.isArray())
        {
            Collection values = this.values();
            for(Object val : values)
            {
                int[] newval = (int[]) val;
                int[] newvalue = (int[]) value;
                if(newval[0] == newvalue[0])
                {
                    return true;
                }
            }
        }
    }
    return false;
}

实际上,HashMap和List对于通过zipcode查找int这样的特定任务来说太笼统了。 您应该利用使用数据的知识。 其中一个选项是使用带有存储int值的叶子的前缀树。 此外,如果(我的猜测)很多具有相同前缀的代码映射到相同的整数,它可以被修剪。

通过zipcode查找int将在这种树中是线性的,并且如果代码数量增加则不会增长,在二进制搜索的情况下与O(log(N))相比。

由于您打算使用散列,因此可以尝试基于ASCII值对字符串进行数值转换。 最简单的想法是

    int sum=0;
    for(int i=0;i<arr.length;i++){
        sum+=(int)arr[i];

    }

使用定义良好的散列函数散列“sum”。 您将使用基于预期输入模式的哈希函数。 例如,如果你使用除法

    public int hasher(int sum){
       return sum%(a prime number);
    }

选择一个不接近精确2次幂的素数可以改善性能并提供更好的均匀散列键分配。

另一种方法是根据各自的位置权衡角色。

例如:如果使用上述方法,“abc”和“cab”都将被散列到同一位置。 但如果您需要将它们存储在两个不同的位置,请为我们使用数字系统的位置提供权重。

     int sum=0;
     int weight=1;
     for(int i=0;i<arr.length;i++){
         sum+= (int)arr[i]*weight;
         weight=weight*2; // using powers of 2 gives better results. (you know why :))
     }  

由于您的样本非常大,因此您可以通过链接机制避免冲突,而不是使用探测序列。 毕竟,您选择的方法完全取决于您的应用程序的性质。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM