关于字符串实习和替代方案
On string interning and alternatives
我有一个大文件,其中包含如下数据:
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,...,...
...
这是一个数 GB 的文件。我有一个 class 读取此文件并将这些行(记录)公开为 IEnumerable<MyObject>
。这个MyObject
有几个属性(Country
,Province
,City
, ...)等等
如您所见,有很多重复数据。我想继续将基础数据公开为 IEnumerable<MyObject>
。但是,其他一些 class 可能(并且可能会)对这些数据进行一些分层 view/structure,例如:
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
};
}
现在,对于手头的实际问题:我可以使用string.Intern()
来实习国家、省、市和街道字符串(这些是主要的'vilains',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()
方法创建层次结构时,相应的键(国家、省等)。字典会更有效率。
然而,使用 string.Intern()
的缺点之一(除了轻微的性能损失,这不是问题)是字符串 won't be garbage collected anymore。但是当我处理完我的数据后,我 想要收集所有这些垃圾(最终)。
I could use a Dictionary<string, string>
to 'intern' this data 但我不喜欢 "overhead" key
和 value
实际上,我只对 [=30= 感兴趣].我可以将 value
设置为 null
或使用相同的字符串作为值(这将在 key
和 value
中产生相同的引用)。这只是几个字节的小代价,但它仍然是一个代价。
HashSet<string>
之类的东西对我来说更有意义。但是,我无法获得对 HashSet 中字符串的引用;我可以查看 HashSet 是否包含 特定字符串,但无法获取对 HashSet 中所定位字符串的特定实例的引用。 I could implement my own HashSet
for this,但我想知道您在 Whosebugers 上可能会想出哪些其他解决方案。
要求:
- 我的 "FileReader" class 需要继续暴露一个
IEnumerable<MyObject>
- 我的"FileReader"class可能做一些事情(比如
string.Intern()
)来优化内存使用
MyObject
class不能改变;我不会制作 City
class、Country
class 等,并让 MyObject
将它们公开为属性而不是简单的 string
属性
- 目标是通过删除
Country
、Province
、City
等中的大部分重复字符串来提高(更多)内存效率;这是如何实现的(例如字符串实习、内部哈希集/集合/某物的结构)并不重要。然而:
- 我知道我可以将数据填充到数据库中或在这方面使用其他解决方案;我对这些解决方案不感兴趣。
- 速度只是次要问题;当然,越快越好,但性能(轻微)损失,而 reading/iterating 对象没问题
- 因为这是一个长期 运行ning 过程(如:windows service 运行ning 24/7/365),偶尔会处理大量此类数据我希望数据在我处理完后被垃圾收集起来;字符串实习效果很好,但在长期 运行 中会导致一个巨大的字符串池,其中包含大量未使用的数据
- 我希望任何解决方案都是 "simple";添加 15 classes with P/Invokes 和内联汇编(夸大)是不值得的。代码可维护性在我的列表中名列前茅。
这更像是一个 'theoretical' 问题;我问这纯粹是出于好奇/兴趣。没有“真正的”问题,但我可以看到在类似情况下这个可能是一个对某人有问题。
例如:我可以这样做:
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
}
}
但是对于大量(要删除重复的)字符串,这将很快陷入困境。我可以看一下 reference source for HashSet or Dictionary 或...并构建一个类似的 class,它不是 return bool 用于 Add()
方法,而是在internals/bucket.
到目前为止我能想到的最好的是:
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);
}
}
其中 "penalty" 有一个键 和 一个我实际上只对键感兴趣的值。虽然只有几个字节,但付出的代价很小。巧合的是,这也减少了 42% 的内存使用量;与使用 string.Intern()
产生的结果相同。
:
public class StringInterningObject
{
private System.Xml.NameTable nt = new System.Xml.NameTable();
public string Add(string value)
{
return nt.Add(value);
}
}
(我删除了 (the latter since the NameTable already does that))
:
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;
}
}
(稍微修改为 "fit" 我的 "Add() interface")
根据:
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;
}
}
我只是想知道是否有 neater/better/cooler 方法可以解决 'solve' 我的(不是那么实际的)问题。 现在我我想有足够的选择
以下是我为一些简单、简短的初步测试得出的一些数字:
未优化
内存:~4,5Gb
加载时间:~52s
StringInterningObject(见上文,ConcurrentDictionary
变体)
内存:~2,6Gb
加载时间:~49s
string.Intern()
内存:~2,3Gb
加载时间:~45s
内存:~2,3Gb
加载时间:~41s
内存:~2,3Gb
加载时间:~58s
StringInterningObject(见上文,(非并发)Dictionary
变体)根据 :
内存:~2 ,3Gb
加载时间:~39s
虽然数字不是很明确,但似乎非优化版本的许多内存分配实际上比使用 string.Intern()
或上面的 StringInterningObject
s 更慢导致(稍微)更长的加载时间。 此外,string.Intern()
似乎从 StringInterningObject
变为 'win',但幅度不大; << 查看更新。
如有疑问,作弊! :-)
public class CachingEqualityComparer<T> : IEqualityComparer<T> where T : class
{
public T X { get; private set; }
public T Y { get; private set; }
public IEqualityComparer<T> DefaultComparer = EqualityComparer<T>.Default;
public bool Equals(T x, T y)
{
bool result = DefaultComparer.Equals(x, y);
if (result)
{
X = x;
Y = y;
}
return result;
}
public int GetHashCode(T obj)
{
return DefaultComparer.GetHashCode(obj);
}
public T Other(T one)
{
if (object.ReferenceEquals(one, X))
{
return Y;
}
if (object.ReferenceEquals(one, Y))
{
return X;
}
throw new ArgumentException("one");
}
public void Reset()
{
X = default(T);
Y = default(T);
}
}
使用示例:
var comparer = new CachingEqualityComparer<string>();
var hs = new HashSet<string>(comparer);
string str = "Hello";
string st1 = str.Substring(2);
hs.Add(st1);
string st2 = str.Substring(2);
// st1 and st2 are distinct strings!
if (object.ReferenceEquals(st1, st2))
{
throw new Exception();
}
comparer.Reset();
if (hs.Contains(st2))
{
string cached = comparer.Other(st2);
Console.WriteLine("Found!");
// cached is st1
if (!object.ReferenceEquals(cached, st1))
{
throw new Exception();
}
}
我创建了一个相等比较器,"caches" 它分析的最后 Equal
个术语:-)
一切都可以封装在 HashSet<T>
的子类中
/// <summary>
/// An HashSet<T;gt; that, thorough a clever use of an internal
/// comparer, can have a AddOrGet and a TryGet
/// </summary>
/// <typeparam name="T"></typeparam>
public class HashSetEx<T> : HashSet<T> where T : class
{
public HashSetEx()
: base(new CachingEqualityComparer<T>())
{
}
public HashSetEx(IEqualityComparer<T> comparer)
: base(new CachingEqualityComparer<T>(comparer))
{
}
public T AddOrGet(T item)
{
if (!Add(item))
{
var comparer = (CachingEqualityComparer<T>)Comparer;
item = comparer.Other(item);
}
return item;
}
public bool TryGet(T item, out T item2)
{
if (Contains(item))
{
var comparer = (CachingEqualityComparer<T>)Comparer;
item2 = comparer.Other(item);
return true;
}
item2 = default(T);
return false;
}
private class CachingEqualityComparer<T> : IEqualityComparer<T> where T : class
{
public WeakReference X { get; private set; }
public 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 WeakReference(x);
Y = new 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;
}
}
}
请注意 WeakReference
的使用,这样就不会出现对可能阻止垃圾回收的对象的无用引用。
使用示例:
var hs = new HashSetEx<string>();
string str = "Hello";
string st1 = str.Substring(2);
hs.Add(st1);
string st2 = str.Substring(2);
// st1 and st2 are distinct strings!
if (object.ReferenceEquals(st1, st2))
{
throw new Exception();
}
string stFinal = hs.AddOrGet(st2);
if (!object.ReferenceEquals(stFinal, st1))
{
throw new Exception();
}
string stFinal2;
bool result = hs.TryGet(st1, out stFinal2);
if (!object.ReferenceEquals(stFinal2, st1))
{
throw new Exception();
}
if (!result)
{
throw new Exception();
}
我确实有这个要求,并且确实在 SO 上询问过,但是 没有 像你的问题的细节一样,没有有用的回复。 中内置的一个选项 是一个 (System.Xml).NameTable,它基本上是一个字符串原子化对象,这正是您正在寻找的,我们有(我们实际上已经转移到 Intern因为我们会为 App-life 保留这些字符串。
if (name == null) return null;
if (name == "") return string.Empty;
lock (m_nameTable)
{
return m_nameTable.Add(name);
}
在私有 NameTable 上
http://referencesource.microsoft.com/#System.Xml/System/Xml/NameTable.cs,c71b9d3a7bc2d2af 显示其实现为简单哈希表,即每个字符串仅存储一个引用。
缺点?是完全特定于字符串的。如果您对内存/速度进行交叉测试,我很想看看结果。我们已经大量使用 System.Xml,如果您不这样做,当然可能看起来不那么自然。
edit3:
而不是索引字符串,将它们放在非重复列表中将节省更多的 ram。
我们在 class MyObjectOptimized 中有 int 索引。访问是即时的。
如果列表很短(如 1000 项),则设置值的速度不会很明显。
i assumed every string will have 5 character .
this will reduce memory usage
percentage : 110 byte /16byte = 9x gain
total : 5gb/9 = 0.7 gb + sizeof(Country_li , Province_li etc )
with int16 index (will further halve ram usage )
*note:* int16 capacity is -32768 to +32767 ,
make sure your list is not bigger than 32 767
用法相同,但将使用 class MyObjectOptimized
main()
{
// you can use same code
foreach (line in myfile) {
fields = line.split(",");
yield
return
new MyObjectOptimized {
Country = fields[0],
Province = fields[1],
City = fields[2],
Street = fields[3],
//...other fields
};
}
}
需要class是
// single string size : 18 bytes (empty string size) + 2 bytes per char allocated
//1 class instance ram cost : 4 * (18 + 2* charCount )
// ie charcounts are at least 5
// cost: 4*(18+2*5) = 110 byte
class MyObject
{
string Country ;
string Province ;
string City ;
string Street ;
}
public static class Exts
{
public static int AddDistinct_and_GetIndex(this List<string> list ,string value)
{
if( !list.Contains(value) ) {
list.Add(value);
}
return list.IndexOf(value);
}
}
// 1 class instance ram cost : 4*4 byte = 16 byte
class MyObjectOptimized
{
//those int's could be int16 depends on your distinct item counts
int Country_index ;
int Province_index ;
int City_index ;
int Street_index ;
// manuallly implemented properties will not increase memory size
// whereas field WILL increase
public string Country{
get {return Country_li[Country_index]; }
set { Country_index = Country_li.AddDistinct_and_GetIndex(value); }
}
public string Province{
get {return Province_li[Province_index]; }
set { Province_index = Province_li.AddDistinct_and_GetIndex(value); }
}
public string City{
get {return City_li[City_index]; }
set { City_index = City_li.AddDistinct_and_GetIndex(value); }
}
public string Street{
get {return Street_li[Street_index]; }
set { Street_index = Street_li.AddDistinct_and_GetIndex(value); }
}
//beware they are static.
static List<string> Country_li ;
static List<string> Province_li ;
static List<string> City_li ;
static List<string> Street_li ;
}
我有一个大文件,其中包含如下数据:
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,...,...
...
这是一个数 GB 的文件。我有一个 class 读取此文件并将这些行(记录)公开为 IEnumerable<MyObject>
。这个MyObject
有几个属性(Country
,Province
,City
, ...)等等
如您所见,有很多重复数据。我想继续将基础数据公开为 IEnumerable<MyObject>
。但是,其他一些 class 可能(并且可能会)对这些数据进行一些分层 view/structure,例如:
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
};
}
现在,对于手头的实际问题:我可以使用string.Intern()
来实习国家、省、市和街道字符串(这些是主要的'vilains',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()
方法创建层次结构时,相应的键(国家、省等)。字典会更有效率。
然而,使用 string.Intern()
的缺点之一(除了轻微的性能损失,这不是问题)是字符串 won't be garbage collected anymore。但是当我处理完我的数据后,我 想要收集所有这些垃圾(最终)。
I could use a Dictionary<string, string>
to 'intern' this data 但我不喜欢 "overhead" key
和 value
实际上,我只对 [=30= 感兴趣].我可以将 value
设置为 null
或使用相同的字符串作为值(这将在 key
和 value
中产生相同的引用)。这只是几个字节的小代价,但它仍然是一个代价。
HashSet<string>
之类的东西对我来说更有意义。但是,我无法获得对 HashSet 中字符串的引用;我可以查看 HashSet 是否包含 特定字符串,但无法获取对 HashSet 中所定位字符串的特定实例的引用。 I could implement my own HashSet
for this,但我想知道您在 Whosebugers 上可能会想出哪些其他解决方案。
要求:
- 我的 "FileReader" class 需要继续暴露一个
IEnumerable<MyObject>
- 我的"FileReader"class可能做一些事情(比如
string.Intern()
)来优化内存使用 MyObject
class不能改变;我不会制作City
class、Country
class 等,并让MyObject
将它们公开为属性而不是简单的string
属性- 目标是通过删除
Country
、Province
、City
等中的大部分重复字符串来提高(更多)内存效率;这是如何实现的(例如字符串实习、内部哈希集/集合/某物的结构)并不重要。然而: - 我知道我可以将数据填充到数据库中或在这方面使用其他解决方案;我对这些解决方案不感兴趣。
- 速度只是次要问题;当然,越快越好,但性能(轻微)损失,而 reading/iterating 对象没问题
- 因为这是一个长期 运行ning 过程(如:windows service 运行ning 24/7/365),偶尔会处理大量此类数据我希望数据在我处理完后被垃圾收集起来;字符串实习效果很好,但在长期 运行 中会导致一个巨大的字符串池,其中包含大量未使用的数据
- 我希望任何解决方案都是 "simple";添加 15 classes with P/Invokes 和内联汇编(夸大)是不值得的。代码可维护性在我的列表中名列前茅。
这更像是一个 'theoretical' 问题;我问这纯粹是出于好奇/兴趣。没有“真正的”问题,但我可以看到在类似情况下这个可能是一个对某人有问题。
例如:我可以这样做:
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
}
}
但是对于大量(要删除重复的)字符串,这将很快陷入困境。我可以看一下 reference source for HashSet or Dictionary 或...并构建一个类似的 class,它不是 return bool 用于 Add()
方法,而是在internals/bucket.
到目前为止我能想到的最好的是:
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);
}
}
其中 "penalty" 有一个键 和 一个我实际上只对键感兴趣的值。虽然只有几个字节,但付出的代价很小。巧合的是,这也减少了 42% 的内存使用量;与使用 string.Intern()
产生的结果相同。
public class StringInterningObject
{
private System.Xml.NameTable nt = new System.Xml.NameTable();
public string Add(string value)
{
return nt.Add(value);
}
}
(我删除了
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;
}
}
(稍微修改为 "fit" 我的 "Add() interface")
根据
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;
}
}
我只是想知道是否有 neater/better/cooler 方法可以解决 'solve' 我的(不是那么实际的)问题。 现在我我想有足够的选择
以下是我为一些简单、简短的初步测试得出的一些数字:
未优化
内存:~4,5Gb
加载时间:~52s
StringInterningObject(见上文,ConcurrentDictionary
变体)
内存:~2,6Gb
加载时间:~49s
string.Intern()
内存:~2,3Gb
加载时间:~45s
内存:~2,3Gb
加载时间:~41s
内存:~2,3Gb
加载时间:~58s
StringInterningObject(见上文,(非并发)Dictionary
变体)根据
内存:~2 ,3Gb
加载时间:~39s
虽然数字不是很明确,但似乎非优化版本的许多内存分配实际上比使用 string.Intern()
或上面的 StringInterningObject
s 更慢导致(稍微)更长的加载时间。 此外, << 查看更新。 string.Intern()
似乎从 StringInterningObject
变为 'win',但幅度不大;
如有疑问,作弊! :-)
public class CachingEqualityComparer<T> : IEqualityComparer<T> where T : class
{
public T X { get; private set; }
public T Y { get; private set; }
public IEqualityComparer<T> DefaultComparer = EqualityComparer<T>.Default;
public bool Equals(T x, T y)
{
bool result = DefaultComparer.Equals(x, y);
if (result)
{
X = x;
Y = y;
}
return result;
}
public int GetHashCode(T obj)
{
return DefaultComparer.GetHashCode(obj);
}
public T Other(T one)
{
if (object.ReferenceEquals(one, X))
{
return Y;
}
if (object.ReferenceEquals(one, Y))
{
return X;
}
throw new ArgumentException("one");
}
public void Reset()
{
X = default(T);
Y = default(T);
}
}
使用示例:
var comparer = new CachingEqualityComparer<string>();
var hs = new HashSet<string>(comparer);
string str = "Hello";
string st1 = str.Substring(2);
hs.Add(st1);
string st2 = str.Substring(2);
// st1 and st2 are distinct strings!
if (object.ReferenceEquals(st1, st2))
{
throw new Exception();
}
comparer.Reset();
if (hs.Contains(st2))
{
string cached = comparer.Other(st2);
Console.WriteLine("Found!");
// cached is st1
if (!object.ReferenceEquals(cached, st1))
{
throw new Exception();
}
}
我创建了一个相等比较器,"caches" 它分析的最后 Equal
个术语:-)
一切都可以封装在 HashSet<T>
/// <summary>
/// An HashSet<T;gt; that, thorough a clever use of an internal
/// comparer, can have a AddOrGet and a TryGet
/// </summary>
/// <typeparam name="T"></typeparam>
public class HashSetEx<T> : HashSet<T> where T : class
{
public HashSetEx()
: base(new CachingEqualityComparer<T>())
{
}
public HashSetEx(IEqualityComparer<T> comparer)
: base(new CachingEqualityComparer<T>(comparer))
{
}
public T AddOrGet(T item)
{
if (!Add(item))
{
var comparer = (CachingEqualityComparer<T>)Comparer;
item = comparer.Other(item);
}
return item;
}
public bool TryGet(T item, out T item2)
{
if (Contains(item))
{
var comparer = (CachingEqualityComparer<T>)Comparer;
item2 = comparer.Other(item);
return true;
}
item2 = default(T);
return false;
}
private class CachingEqualityComparer<T> : IEqualityComparer<T> where T : class
{
public WeakReference X { get; private set; }
public 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 WeakReference(x);
Y = new 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;
}
}
}
请注意 WeakReference
的使用,这样就不会出现对可能阻止垃圾回收的对象的无用引用。
使用示例:
var hs = new HashSetEx<string>();
string str = "Hello";
string st1 = str.Substring(2);
hs.Add(st1);
string st2 = str.Substring(2);
// st1 and st2 are distinct strings!
if (object.ReferenceEquals(st1, st2))
{
throw new Exception();
}
string stFinal = hs.AddOrGet(st2);
if (!object.ReferenceEquals(stFinal, st1))
{
throw new Exception();
}
string stFinal2;
bool result = hs.TryGet(st1, out stFinal2);
if (!object.ReferenceEquals(stFinal2, st1))
{
throw new Exception();
}
if (!result)
{
throw new Exception();
}
我确实有这个要求,并且确实在 SO 上询问过,但是 没有 像你的问题的细节一样,没有有用的回复。 中内置的一个选项 是一个 (System.Xml).NameTable,它基本上是一个字符串原子化对象,这正是您正在寻找的,我们有(我们实际上已经转移到 Intern因为我们会为 App-life 保留这些字符串。
if (name == null) return null;
if (name == "") return string.Empty;
lock (m_nameTable)
{
return m_nameTable.Add(name);
}
在私有 NameTable 上
http://referencesource.microsoft.com/#System.Xml/System/Xml/NameTable.cs,c71b9d3a7bc2d2af 显示其实现为简单哈希表,即每个字符串仅存储一个引用。
缺点?是完全特定于字符串的。如果您对内存/速度进行交叉测试,我很想看看结果。我们已经大量使用 System.Xml,如果您不这样做,当然可能看起来不那么自然。
edit3:
而不是索引字符串,将它们放在非重复列表中将节省更多的 ram。
我们在 class MyObjectOptimized 中有 int 索引。访问是即时的。 如果列表很短(如 1000 项),则设置值的速度不会很明显。
i assumed every string will have 5 character .
this will reduce memory usage
percentage : 110 byte /16byte = 9x gain
total : 5gb/9 = 0.7 gb + sizeof(Country_li , Province_li etc )
with int16 index (will further halve ram usage )
*note:* int16 capacity is -32768 to +32767 ,
make sure your list is not bigger than 32 767
用法相同,但将使用 class MyObjectOptimized
main()
{
// you can use same code
foreach (line in myfile) {
fields = line.split(",");
yield
return
new MyObjectOptimized {
Country = fields[0],
Province = fields[1],
City = fields[2],
Street = fields[3],
//...other fields
};
}
}
需要class是
// single string size : 18 bytes (empty string size) + 2 bytes per char allocated
//1 class instance ram cost : 4 * (18 + 2* charCount )
// ie charcounts are at least 5
// cost: 4*(18+2*5) = 110 byte
class MyObject
{
string Country ;
string Province ;
string City ;
string Street ;
}
public static class Exts
{
public static int AddDistinct_and_GetIndex(this List<string> list ,string value)
{
if( !list.Contains(value) ) {
list.Add(value);
}
return list.IndexOf(value);
}
}
// 1 class instance ram cost : 4*4 byte = 16 byte
class MyObjectOptimized
{
//those int's could be int16 depends on your distinct item counts
int Country_index ;
int Province_index ;
int City_index ;
int Street_index ;
// manuallly implemented properties will not increase memory size
// whereas field WILL increase
public string Country{
get {return Country_li[Country_index]; }
set { Country_index = Country_li.AddDistinct_and_GetIndex(value); }
}
public string Province{
get {return Province_li[Province_index]; }
set { Province_index = Province_li.AddDistinct_and_GetIndex(value); }
}
public string City{
get {return City_li[City_index]; }
set { City_index = City_li.AddDistinct_and_GetIndex(value); }
}
public string Street{
get {return Street_li[Street_index]; }
set { Street_index = Street_li.AddDistinct_and_GetIndex(value); }
}
//beware they are static.
static List<string> Country_li ;
static List<string> Province_li ;
static List<string> City_li ;
static List<string> Street_li ;
}