关于字符串实习和替代

时间:2015-05-01 09:58:53

标签: c# .net string hashset string-interning

我有一个大文件,实质上包含如下数据:

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有多个属性(CountryProvinceCity,...)等。

正如您所看到的,有很多重复的数据。我希望将基础数据公开为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
    };
}

现在,针对手头的实际问题:我可以使用string.Intern()实习国家,省,城市和街道字符串(这些是主要&#39; vilains&# 39;,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()方法创建分层结构时,可以使用resp的密钥(Country,Province等)。字典会更有效率。

然而,使用string.Intern()的一个缺点(除了轻微的性能损失,这不是问题)是字符串won't be garbage collected anymore。但是,当我完成我的数据后,我希望收集所有垃圾(最终)。

I could use a Dictionary<string, string> to 'intern' this data但我不喜欢&#34;开销&#34;我实际上只有keyvalue才对key感兴趣。我可以将value设置为null或使用与值相同的字符串(这将在keyvalue中生成相同的引用)。它只需支付几个字节的小价格,但它仍然是一个价格。

HashSet<string>之类的东西对我来说更有意义。但是,我无法在HashSet中获取对字符串的引用;我可以看到HashSet 是否包含特定的字符串,但是没有获得对HashSet中所定位字符串的特定实例的引用。 I could implement my own HashSet for this,但我想知道StackOverflowers可能提出的其他解决方案。

要求:

  • 我的&#34; FileReader&#34; class需要继续暴露IEnumerable<MyObject>
  • 我的&#34; FileReader&#34; class 可以做一些事情(比如string.Intern())来优化内存使用
  • MyObject无法更改;我不会创建City课程,Country课程等,并MyObject将这些内容公开为属性,而不是简单的string属性
  • 通过重复删除CountryProvinceCity等中的大多数重复字符串,目标是提高(更多)内存效率。如何实现这一点(例如字符串实习,内部哈希集/集合/某事物的结构)并不重要。但是:
  • 我知道我可以将数据填入数据库或使用其他解决方案;我对这些解决方案感兴趣。
  • 速度只是次要问题;读取/迭代对象时,性能越快越好但性能(轻微)损失没问题
  • 由于这是一个长时间运行的过程(如:运行24/7/365的Windows服务),偶尔会处理大量此类数据,我希望在完成后对数据进行垃圾回收用它; string interning工作得很好,但从长远来看,会产生一个包含大量未使用数据的巨大字符串池
  • 我希望任何解决方案都是简单的&#34 ;;使用P / Invokes和内联汇编(夸大)添加15个类是不值得的。代码可维护性在我的列表中很高。

这更像是一个理论上的&#39;题;纯粹是出于好奇/兴趣,我一直在问。没有&#34; 真正的&#34;问题,但我可以看到在类似的情况下,可能对某人来说是一个问题。

例如:我可以这样做:

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 HashSetDictionary或...并构建一个类似的类,它不会为Add()方法返回bool但是在内部/铲斗。

我能想到的最好的东西是:

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);
    }
}

其中有#34;惩罚&#34;有一个Key 一个值,我实际上只对Key感兴趣。只需几个字节,支付的价格很小。顺便说一句,这也减少了42%的内存使用量;与使用string.Intern()时产生的结果相同。

tolanj came up with System.Xml.NameTable

public class StringInterningObject
{
    private System.Xml.NameTable nt = new System.Xml.NameTable();

    public string Add(string value)
    {
        return nt.Add(value);
    }
}

(我删除了lock and string.Empty check(后者,因为NameTable already does that))

xanatos came up with a 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;
    }
}

(稍微修改为&#34;适合&#34;我的&#34;添加()界面&#34;)

根据Henk Holterman's request

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;
    }
}

我只是想知道是否有一种更整洁/更好/更酷的方式来解决问题&#39;我(实际上不是那么多)问题。现在我有足够的选择我猜wink

以下是我想出的一些简单,简短的初步测试数据:


非优化
内存:~4,5Gb
加载时间:~52s


StringInterningObject (见上文,ConcurrentDictionary变体)
内存:~2,6Gb
加载时间:~49s


string.Intern()
内存:~2,3Gb
加载时间:~45s


System.Xml.NameTable
内存:~2,3Gb
加载时间:~41s


CachingEqualityComparer
内存:~2,3Gb
加载时间:~58s


StringInterningObject (见上文,(非并发)Dictionary变体)按Henk Holterman's request
内存:~2,3Gb 加载时间:~39秒

尽管这些数字并不是非常明确的,但似乎非优化版本的许多内存分配实际上比使用string.Intern()或上述StringInterningObject更慢。导致(稍微)更长的加载时间。 此外,string.Intern()似乎“赢了”。来自StringInterningObject,但不是很大; &lt;&lt;查看更新。

3 个答案:

答案 0 :(得分:3)

如有疑问,请作弊! : - )

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();
    }
}

我创建了一个“缓存”其分析的最后Equal个术语的相等比较器: - )

然后可以将所有内容封装在HashSet<T>

的子类中
/// <summary>
/// An HashSet&lt;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();
}

答案 1 :(得分:3)

我确实已经满足了这个要求并且确实在SO上询问过,但是没有就像问题的详细信息一样,没有有用的回复。一个用构建的选项是一个(System.Xml).NameTable,它基本上是一个字符串雾化对象,这是你正在寻找的,我们有(我们实际上转移到实习生,因为我们为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显示它实现为Simple哈希表,即每个字符串只存储一个引用。

下行?是完全字符串特定的。如果你对内存/速度进行交叉测试,我有兴趣看到结果。我们已经在大量使用System.Xml,如果你不在的话,当然可能不那么自然。

答案 2 :(得分:0)

TLDR :从当前结果来看,我使用string.intern,  基于字典的实习生没有多大作用

edit2:

我发布我的制作结果

public class TProduct
{
    public int ProductId { get; set; }
    public int BrandId { get; set; }
    public string ProductName { get; set; }
    public string WordName { get; set; }
    public string BrandName { get; set; }
    public string ProductNameClean { get; set; }  //interned field
    public string BrandNameClean { get; set; }    //interned field

}

该领域的2/7被拘禁了,也许这就是为什么结果不如我预期的那么好。 (如果增加复杂性,我希望至少可以减少2-3倍)

  • 2.5M级产品
  • 50.000个不同的brandName
  • 50.000个不同的brandnameclean
                                   |  reduction  |
    |                     | ram mb |  mb  |  %   |                  |
    |---------------------|--------|------|------|------------------|
    | string              | 680    |      |      |                  |
    | string.intern       | 513    | 167  | 25   |                  |
    | string.intern(dict) | 500    | 154  | 26   |                  |
    | byte[]              | 486    | 194  | 29   | hard to maintain |
    | byte[].CustomIntern | 447    | 233  | 34   | hard to maintain |

我还必须将byteArrayEqualityComparer添加到byte []。customIntern中。 否则等于gethashcode无法正常工作。


编辑1:

如果ascii(256个字符的英语-英语字符)适合您。

将所有字符串转换为byte []。并使用byte [] intern。

但这可能会带来很多无法预料的问题。

还可能需要获取char数组,并检查其数值是否大于256。请确保抛出异常。如果您的记录列表字母组合很好,那么

从数据库到byteClass

var AllDbResults_2mRec = new List<MyByteClass>();

foreach (var fields in DbRowProvider)
   AllDbResults_2mRec.Add(
   new MyClass {
        Country = byteArrayInterningObject.Intern(fields[0].ASCII_bytes() ),
        Province = byteArrayInterningObject.Intern(fields[1].ASCII_bytes() ),
        City = byteArrayInterningObject.Intern(fields[2].ASCII_bytes() ),
      } );

当您搜索200万个MyByteClass记录时。
您过滤掉了20条记录(例如)

MyByteClass[] results_asByte =  AllDbResults_2mRec .Search("tokyo");

MyClass[] results = results_asByte
                    .Select(x=> MyClass.From_Byte(x) )
                    .ToArray();   

必修课程

class MyClass
{
    string[] Country ;
    string[] Province;
    string[] City ;

   public static From_Byte(MyByteClass  mbc)
   {
      return new MyClass {
        Country = mbc.Country.ASCII_string() ),
        Province = mbc.Province.ASCII_string() ),
        City = mbc.City.ASCII_string() ),
      };
   }  

 }

class MyByteClass
{
    byte[] Country ;
    byte[] Province;
    byte[] City ;

 }

public static class AsciiExt
{
// guarantee single byte per char. other one return multi byte for single char  like € æ ß  im not sure . but i chnaged it in my production code
public static byte[] ASCII_Bytes(this string str)
{      
    if (str == null)
        return new byte[0];

    var byteArr = new byte[str.Length];
    for (int i = 0; i < str.Length; i++)
    {
        byteArr[i] = (byte)str[i]; //utf16 - already ascii compliant??
    }

    return byteArr;
}



    public static byte[] ASCII_bytes(this string str)
    {
        return str == null ?
         new byte[0] : Encoding.ASCII.GetBytes(str);
    }
    public static string ASCII_String(this byte[] byteArr)
    {
        return byteArr == null ?
         string.Empty :
         Encoding.ASCII.GetString(byteArr);
    }
}


public class byteArrayInterningObject
{
    private Dictionary<byte[], byte[]> _items;

    public byteArrayInterningObject()
    {
        _items = new Dictionary<byte[], byte[]>();
    }

    public string Add(byte[] value)
    {
        string result;
        if (!_items.TryGetValue(value, out result))
        {
            _items.Add(value, value);
            return value;
        }
        return result;
    }
}