本質的に次のようなデータを含む大きなファイルがあります。
Netherlands,Noord-holland,Amsterdam,FooStreet,1,...,...
Netherlands,Noord-holland,Amsterdam,FooStreet,2,...,...
Netherlands,Noord-holland,Amsterdam,FooStreet,3,...,...
Netherlands,Noord-holland,Amsterdam,FooStreet,4,...,...
Netherlands,Noord-holland,Amsterdam,FooStreet,5,...,...
Netherlands,Noord-holland,Amsterdam,BarRoad,1,...,...
Netherlands,Noord-holland,Amsterdam,BarRoad,2,...,...
Netherlands,Noord-holland,Amsterdam,BarRoad,3,...,...
Netherlands,Noord-holland,Amsterdam,BarRoad,4,...,...
Netherlands,Noord-holland,Amstelveen,BazDrive,1,...,...
Netherlands,Noord-holland,Amstelveen,BazDrive,2,...,...
Netherlands,Noord-holland,Amstelveen,BazDrive,3,...,...
Netherlands,Zuid-holland,Rotterdam,LoremAve,1,...,...
Netherlands,Zuid-holland,Rotterdam,LoremAve,2,...,...
Netherlands,Zuid-holland,Rotterdam,LoremAve,3,...,...
...
これは数ギガバイトのファイルです。このファイルを読み取り、これらの行 (レコード) をIEnumerable<MyObject>
. これMyObject
には、いくつかのプロパティ ( Country
、Province
、City
、...) などがあります。
ご覧のとおり、データの重複がたくさんあります。基になるデータを として公開し続けたいと思いIEnumerable<MyObject>
ます。ただし、他のクラスでは、次のようなこのデータの階層ビュー/構造を作成する可能性があります (おそらくそうするでしょう)。
Netherlands
Noord-holland
Amsterdam
FooStreet [1, 2, 3, 4, 5]
BarRoad [1, 2, 3, 4]
...
Amstelveen
BazDrive [1, 2, 3]
...
...
Zuid-holland
Rotterdam
LoremAve [1, 2, 3]
...
...
...
...
このファイルを読むときは、基本的に次のようにします。
foreach (line in myfile) {
fields = line.split(",");
yield return new MyObject {
Country = fields[0],
Province = fields[1],
City = fields[2],
Street = fields[3],
//...other fields
};
}
さて、手元にある実際の質問に: Country、Province、City、Street の文字列をインターンするために使用できます (これらは主な「悪役」であり、質問に関係のない他のいくつかのプロパティがあります)。string.Intern()
MyObject
foreach (line in myfile) {
fields = line.split(",");
yield return new MyObject {
Country = string.Intern(fields[0]),
Province = string.Intern(fields[1]),
City = string.Intern(fields[2]),
Street = string.Intern(fields[3]),
//...other fields
};
}
これにより、すべての重複文字列が同じ文字列への参照になるため、データセット全体をメモリに保持すると、約 42% のメモリ (テストおよび測定) が節約されます。また、多くの LINQ の.ToDictionary()
メソッドで階層構造を作成する場合、それぞれのキー (Country、Province など) を使用します。辞書ははるかに効率的になります。
ただし、使用の欠点の 1 つ (問題ではないパフォーマンスのわずかな損失は別として) はstring.Intern()
、文字列がガベージ コレクションされなくなることです。しかし、データの処理が終わったら、(最終的には) ガベージをすべて収集したいと思います。
を使用してこのデータを「インターン」することもできDictionary<string, string>
ますが、 を使用することの「オーバーヘッド」が好きではなくkey
、value
実際には にのみ関心がありkey
ます。を設定value
するnull
か、値として同じ文字列を使用することができます (結果として と で同じ参照にkey
なりvalue
ます)。支払うのは数バイトのわずかな代償ですが、それでも代償です。
のようなものは、HashSet<string>
私にとってより理にかなっています。ただし、HashSet 内の文字列への参照を取得できません。HashSet に特定の文字列が含まれているかどうかはわかりますが、HashSet 内にある文字列の特定のインスタンスへの参照は取得できません。私はこれのために自分自身を実装することができHashSet
ましたが、あなたが種類の StackOverflowers を思い付くことができる他の解決策を考えています.
要件:
- 私の「FileReader」クラスは、
IEnumerable<MyObject>
- 私の「FileReader」クラスは、メモリ使用量を最適化するために(のような)ことをするかもしれません
string.Intern()
MyObject
クラスは変更できません。City
クラス、Country
クラスなどを作成せず、それらを単純なプロパティMyObject
ではなくプロパティとして公開しますstring
Country
目標は、Province
、City
などの重複文字列のほとんどを重複排除することにより、(より) メモリ効率を高めることです。これがどのように達成されるか (例: 文字列インターン、内部ハッシュセット / コレクション / 何かの構造) は重要ではありません。でも:- データベースにデータを詰め込むか、そのような方向で他のソリューションを使用できることを知っています。この種のソリューションには興味がありません。
- 速度は二次的な問題にすぎません。もちろん、速ければ速いほど良いですが、オブジェクトの読み取り/反復中のパフォーマンスの(わずかな)低下は問題ありません
- これは長時間実行されるプロセス (例: 24 時間 365 日実行されている Windows サービス) であるため、時折、このデータの大量を処理します。文字列インターニングはうまく機能しますが、長期的には、未使用のデータがたくさんある巨大な文字列プールになります
- 解決策は「シンプル」にしたいと思います。P/Invokes とインライン アセンブリ (誇張されています) を使用して 15 個のクラスを追加することは、努力する価値がありません。コードの保守性は私のリストの上位にあります。
これは「理論的な」質問です。私が尋ねているのは純粋に好奇心/興味からです. 「本当の」問題はありませんが、同様の状況でこれが誰かの問題になる可能性があることがわかります。
例: 次のようなことができます。
public class StringInterningObject
{
private HashSet<string> _items;
public StringInterningObject()
{
_items = new HashSet<string>();
}
public string Add(string value)
{
if (_items.Add(value))
return value; //New item added; return value since it wasn't in the HashSet
//MEH... this will quickly go O(n)
return _items.First(i => i.Equals(value)); //Find (and return) actual item from the HashSet and return it
}
}
しかし、(重複除去する) 文字列のセットが大きいと、これはすぐに行き詰まります。HashSetまたはDictionaryまたは ...の参照ソースをのぞいて、メソッドの bool を返さずAdd()
、内部/バケットで見つかった実際の文字列を返す同様のクラスを構築できます。
今まで思いついた最高のものは次のようなものです:
public class StringInterningObject
{
private ConcurrentDictionary<string, string> _items;
public StringInterningObject()
{
_items = new ConcurrentDictionary<string, string>();
}
public string Add(string value)
{
return _items.AddOrUpdate(value, value, (v, i) => i);
}
}
これには、実際にはキーのみに関心があるキーと値を持つという「ペナルティ」があります。ほんの数バイトですが、支払う代償はわずかです。偶然にも、これによりメモリ使用量も 42% 減少します。string.Intern()
yieldsを使用した場合と同じ結果になります。
tolanj は System.Xml.NameTable を思いつきました:
public class StringInterningObject
{
private System.Xml.NameTable nt = new System.Xml.NameTable();
public string Add(string value)
{
return nt.Add(value);
}
}
(ロックとstring.Emptyチェックを削除しました(後者はNameTableがすでにそれを行っているためです))
xanatos は CachingEqualityComparer を思いつきました:
public class StringInterningObject
{
private class CachingEqualityComparer<T> : IEqualityComparer<T> where T : class
{
public System.WeakReference X { get; private set; }
public System.WeakReference Y { get; private set; }
private readonly IEqualityComparer<T> Comparer;
public CachingEqualityComparer()
{
Comparer = EqualityComparer<T>.Default;
}
public CachingEqualityComparer(IEqualityComparer<T> comparer)
{
Comparer = comparer;
}
public bool Equals(T x, T y)
{
bool result = Comparer.Equals(x, y);
if (result)
{
X = new System.WeakReference(x);
Y = new System.WeakReference(y);
}
return result;
}
public int GetHashCode(T obj)
{
return Comparer.GetHashCode(obj);
}
public T Other(T one)
{
if (object.ReferenceEquals(one, null))
{
return null;
}
object x = X.Target;
object y = Y.Target;
if (x != null && y != null)
{
if (object.ReferenceEquals(one, x))
{
return (T)y;
}
else if (object.ReferenceEquals(one, y))
{
return (T)x;
}
}
return one;
}
}
private CachingEqualityComparer<string> _cmp;
private HashSet<string> _hs;
public StringInterningObject()
{
_cmp = new CachingEqualityComparer<string>();
_hs = new HashSet<string>(_cmp);
}
public string Add(string item)
{
if (!_hs.Add(item))
item = _cmp.Other(item);
return item;
}
}
(私の「Add()インターフェース」に「フィット」するようにわずかに変更されました)
public class StringInterningObject
{
private Dictionary<string, string> _items;
public StringInterningObject()
{
_items = new Dictionary<string, string>();
}
public string Add(string value)
{
string result;
if (!_items.TryGetValue(value, out result))
{
_items.Add(value, value);
return value;
}
return result;
}
}
私の(実際の多くではない)問題を「解決」するための、よりきちんとした/より良い/よりクールな方法があるかどうか疑問に思っています。今では十分な選択肢があると思います
以下は、単純で短い予備テストのために私が思いついたいくつかの数値です。
最適化され
ていないメモリ: ~4,5Gb
読み込み時間: ~52秒
StringInterningObject (上記のConcurrentDictionary
バリアントを参照)
メモリ: ~2,6Gb
読み込み時間: ~49 秒
string.Intern()
メモリ: ~2,3Gb
読み込み時間: ~45 秒
System.Xml.NameTable
メモリ: ~2,3Gb
読み込み時間: ~41 秒
CachingEqualityComparer
メモリ: ~2,3Gb
読み込み時間: ~58 秒
Henk Holterman の要求によるStringInterningObject (上記の (非並行)Dictionary
バリアントを参照) :メモリ: ~2,3Gb読み込み時間: ~39 秒
数値はあまり決定的なものではありませんが、最適化されていないバージョンの多くのメモリ割り当ては、実際にはどちらかstring.Intern()
または上記StringInterningObject
の s を使用するよりも遅くなり、ロード時間が (わずかに) 長くなるようです。また、<< 更新を参照してください。string.Intern()
「勝つ」ように見えStringInterningObject
ますが、大差ではありません。