簡體   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