[英]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問題是相關的(但與此相同)。
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庫已經為基元優化了HashMap
和HashSet
類。 在這種情況下, TObjectIntHashMap<String>
會將參數化對象( String
)映射到基本int
。
首先,您是否測量到LinkedList
確實比HashMap
更具內存效率,或者您是如何得出這個結論的? 其次, LinkedList
的元素訪問時間為O(n)
,因此您無法對其進行有效的二進制搜索。 如果你想做這樣的方法,你應該使用一個ArrayList
,它可以讓你在性能和空間之間做出妥協。 然而,我再次懷疑HashMap
, HashTable
或者 - 特別是 - TreeMap
將消耗更多的內存,但前兩個將提供常量訪問和樹映射對數,並提供一個比普通列表更好的接口。 我會嘗試做一些測量,內存消耗的差異究竟是多少。
更新 :正如Adamski指出的那樣, String
本身,而不是它們存儲的數據結構,將消耗最多的內存,查看特定於字符串的數據結構可能是個好主意,例如嘗試 (特別是patricia嘗試 ),這可能會減少字符串所需的存儲空間。
你看過嘗試了嗎? 我沒有使用它們,但它們可能適合你正在做的事情。
自定義樹將具有與O(log n)
相同的復雜性,請勿打擾。 你的解決方案是合理的,但我會使用ArrayList
而不是LinkedList
因為鏈表每個存儲值分配一個額外的對象,這相當於你的案例中的很多對象。
正如Erick所寫,使用Trove庫是一個很好的起點,因為你在存儲int
原語而不是Integer
s中節省了空間。
但是,您仍然面臨存儲200萬個String實例的問題。 鑒於這些是地圖中的關鍵,實習他們不會提供任何好處,所以接下來我要考慮的是是否有一些可以被利用的字符串的特征。 例如:
String
表示常用單詞的句子,那么您可以將String轉換為Sentence
類,並實習單個單詞。 MyString
。 顯然,這里的權衡是執行查找所花費的額外時間。 也許你可以使用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.