.net字符串类替代

时间:2011-03-25 17:20:26

标签: .net string memory-management

由于我正在计划一个应用程序将其中的大量数据保存在内存中,我希望有一种“紧凑”的字符串类,至少有一个字符串格式不大于零终止的ASCII版本字符串。

你知道任何这样的字符串类实现 - 它应该有一些实用函数,如原始字符串类。

编辑:

我需要对字符串进行排序并能够扫描它们,只是提到我将要使用的一些操作。

理想情况下,它与System.String的源兼容,因此基本的搜索和替换操作将优化应用程序内存占用。

NUMBERS:

我可以拥有每条记录的100k记录,最多10个字符串,包含30-60个字符。所以:

100000x10x60 = 60000000 = 57mega字符。为什么不使用60兆公羊而不是120兆公羊?操作会更快,一切都会更紧。

树将用于搜索,但对我计划进行的正则表达式扫描没有帮助。

10 个答案:

答案 0 :(得分:51)

编辑:我现在有一个blog post on this topic,其中有更详细的内容。


按你的数字说明:

  

我可以拥有每条记录的100k记录,最多10个字符串,包含30-60个字符。

让我们从添加对象开销开始 - 由于不可避免的对象开销,一个字符串占用大约20个字节(IIRC - 在64位CLR上可能更多)加上实际数据和长度。让我们再做一次数学运算:

使用字符串:20 + 120字节的100万个对象= 140MB

使用新类:20 + 60字节的100万个对象= 80MB

当然还有60MB的差异,但比例低于您的预期。你只节省了42%的空间而不是50%。

现在,你谈论更快的事情:鉴于CLR本身知道string,我怀疑第三方类无法匹配其某些操作的速度,而你我必须把很多的工作放进去让其他许多人保持同样的速度。不可否认,你具有更好的缓存一致性,如果你可以忽略文化问题,那么通过进行所有比较都可以节省一些时间。

为了60MB,我不会打扰。这些日子差别很小 - 考虑一下,为了弥补使用两种不同字符串类型的显着额外成本,你需要多少客户才能获得这么少的客户。

说完所有这些之后,我很想自己实施它作为像Edulinq这样的博客项目。不要指望任何数周或数月的结果:)

编辑:我刚刚想到了另一个问题。我们上面得到的数字实际上并不正确......因为字符串类是特殊的。它将数据直接嵌入到对象中 - 与数组之外的任何其他数据类型不同,string实例的大小不固定;它根据其中的数据而有所不同。

编写自己的AsciiString类,你将无法做到这一点 - 你必须在类中嵌入一个数组引用:

public class AsciiString
{
    private readonly byte[] data;
}

这意味着您需要额外的4或8个字节用于引用(32或64位CLR)以及每个字符串的数组对象(16个字节,IIRC)的额外开销。

如果你像Java一样设计它,那么获取一个子字符串可以重用现有的字节数组(两个字符串可以共享),但是你需要AsciiString内的额外长度和偏移量。你也会失去一些缓存一致性的好处。

可以只使用原始字节数组作为数据结构并编写一堆扩展方法来对它们采取行动......但那会很可怕,因为那时你无法区分它们在正常字节数组和一个用于表示ASCII字符串的数组之间。

另一种可能性是创建一个这样的结构:

struct AsciiString
{
    private readonly byte[] data;
    ...
}

这样可以有效地为您提供强力打字,但您需要考虑以下事项:

AsciiString x = new AsciiString();

最终将使用null data引用。您可以有效地将此视为x为空值,但它非常非惯用。

答案 1 :(得分:13)

我实际上遇到了类似的问题,但问题参数有些不同。 我的应用程序涉及两种类型的字符串 - 相对较短的字符串,测量60-100个字符,较长的字符串,100-1000字节(平均大约300字节)。

我的用例还必须支持unicode文本,但相对较小比例的字符串实际上有非英语字符。

在我的用例中,我将每个String属性公开为本机String,但底层数据结构是一个包含unicode字节的byte []。

我的用例还需要搜索和排序这些字符串,获取子字符串和其他常见的字符串操作。我的数据集测量数百万。

基本实现如下所示:

byte[] _myProperty;

public String MyProperty
{

   get 
   { 
        if (_myProperty== null)
            return null;

        return Encoding.UTF8.GetString(value);
   }

   set
   { 
        _myProperty = Encoding.UTF8.GetBytes(value);

   }

}

即使您进行搜索和排序,这些转化的效果也相对较小(约为10-15%)。

暂时没问题,但我想进一步减少开销。 下一步是为给定对象中的所有字符串创建一个合并数组(一个对象将包含1个短字符串和1个长字符串,或4个短字符串和1个长字符串)。 所以每个对象都会有一个byte [],每个字符串只需要1个字节(保存它们的长度总是<256)。即使你的字符串可以超过256,并且int仍然比字节[]的12-16字节开销便宜。

这减少了大部分字节[]开销,并增加了一点复杂性但对性能没有额外影响(与所涉及的数组副本相比,编码传递相对昂贵)。

这个实现看起来像这样:

byte _property1;
byte _property2;
byte _proeprty3;

private byte[] _data; 

byte[] data;
//i actually used an Enum to indicate which property, but i am sure you get the idea
private int GetStartIndex(int propertyIndex)
{  

   int result = 0;
   switch(propertyIndex)
   {
       //the fallthrough is on purpose 
       case 2:
          result+=property2;
       case 1:
          result+=property1;

   }

   return result;
}

private int GetLength(int propertyIndex)
{
   switch (propertyIndex)
   {
     case 0:
        return _property1;
     case 1: 
        return _property2;
     case 2:
        return _property3;
   }
    return -1;
}

private String GetString(int propertyIndex)
{
   int startIndex = GetStartIndex(propertyIndex);
   int length = GetLength(propertyIndex);
   byte[] result = new byte[length];
   Array.Copy(data,startIndex,result,0,length);

   return Encoding.UTF8.GetString(result);

}

所以getter看起来像这样:

public String Property1
{
   get{ return GetString(0);}
}

setter具有相同的精神 - 将原始数据复制到两个数组中(0开始到startIndex,以及startIndex和length到length之间),并创建一个包含3个数组的新数组(dataAtStart + NewData + EndData)并将数组的长度设置为适当的局部变量。

我仍然不满意节省的内存和每个属性的手动实现的非常繁重的工作,所以我构建了一个内存压缩分页系统,它使用惊人的快速QuickLZ来压缩整页。 这让我对时间 - 内存权衡(基本上就是页面的大小)有很多控制。

我的用例的压缩率(与更高效的byte []存储相比)接近50%(!)。我使用每页大约10个字符串的页面大小并将相似的属性组合在一起(往往具有相似的数据)。 这增加了10-20%的额外开销(在编码/解码过程之上仍然需要)。分页机制将最近访问的页面缓存到可配置的大小。 即使没有压缩,此实现也允许您为每个页面的开销设置固定因子。 我当前实现页面缓存的主要缺点是使用压缩它不是线程安全的(没有它就没有这样的问题)。

如果您对压缩的分页机制感兴趣,请告诉我(我一直在寻找开源的借口)。

答案 2 :(得分:6)

备用数据结构

我建议,鉴于您希望搜索存储的“字符串”值,您应该考虑是否为Trie结构(如Patricia Trie),或者为了更好的内存分摊,是指向非循环字图(指的是作为DAWG的职责会更好。

构建它们需要更长的时间(尽管通常它们用于底层存储本身代表这种形式相当好的情况,允许预先快速构建)并且即使它们上的某些操作在算法上更优越,您可能会发现在您的真实中世界使用的东西实际上更慢,只要有合理的重复次数,它们会显着减少数据的内存占用。

这些可以被视为字符串实习的.net(以及java和许多其他托管语言)中提供的(内置)de duplification的概括。

如果您特别希望以字典方式保留字符串的顺序(因此您一次只需要考虑一个字符或代码点),那么Patricia Trie可能是更好的选择,在顶部执行排序DAWG会有问题。

如果你有一个特定的字符串域,包括:

,可能会有更多深奥的解决方案

运行长度编码和其他形式的压缩。

以随机访问字符串为代价,如果输入结果不符合预期,则实际使用更多内存的风险。霍夫曼编码往往在英文文本上运行良好,并且很容易实现,它的优点是只要字母的频率分布具有可比性,它的字典就可以在集合中的所有字体中进行分片。排序会再次成为问题。

固定长度字符串。

如果您知道字符串很小,并且所有几乎相同(或完全相同)的大小都可以存储在固定大小的值中(如果字符数在16或更小的范围内,则可以根据需要使用结构) (此处的使用限制取决于您的确切用法,可能在很大程度上取决于您调整代码以便与此设计相匹配的意愿)

答案 3 :(得分:5)

您可以创建一个新的数据结构来保存这些,但我认为这样做太过分了。

但是,如果你有一个每个单词或常用短语的数组,那么你将索引存储为每个单词的数组。

然后你为每个单词支付4个字节,但如果每个单词平均为3.6个字符,那么平均每个单词就节省了3.2个字节,因为你需要支付2个字节/字母一次/单词。 / p>

但是,为了进行搜索或排序,您必须至少在短时间内重建字符串才能获得巨大的性能提升。

您可能想重新考虑如何设计程序,因为有许多程序使用大量数据并且可以在相对受限的内存中运行。

答案 4 :(得分:4)

嗯,有UTF8Encoding

//Example from MSDN
using System;
using System.Text;

public class Example
{
   public static void Main()
   {
      Encoding enc = new UTF8Encoding(true, true);
      string value = "\u00C4 \uD802\u0033 \u00AE"; 

      try
      {
         byte[] bytes= enc.GetBytes(value);
         foreach (var byt in bytes)
            Console.Write("{0:X2} ", byt);
         Console.WriteLine();

         string value2 = enc.GetString(bytes);
         Console.WriteLine(value2);
      }
      catch (EncoderFallbackException e)
      {
         //Encoding error
      }                     
   }
}

然而,就像Jon说的那样,任何时候你想要将它用于任何需要字符串的方法(大多数.Net库),你都必须将它转换回普通的unicode字符串...如果你给了我们更多关于你想要做什么的信息,也许我们可以帮助你找到更好的解决方案?

或者,如果你真的需要低级字节数组不可国际化的以null结尾的字符串,那么你最好用C ++编写它。

答案 5 :(得分:4)

您期待多少重复?如果您的数组中有大量重复项,您可能需要考虑实现一个字符串缓存(Dictionary<string, string>周围的包装器)来缓存特定字符串的实例,并为缓存在其中的每个重复字符串返回该实例的引用

您可以将其与检查实习字符串结合起来,因此如果您在整个程序中共享了大量字符串,则始终使用实习版本。

根据您的数据,这可能会比尝试优化每个字符串的存储空间提供更好的结果。

答案 6 :(得分:1)

我认为关键是每条记录都有很多字符串字段 ...

通过将每个记录的所有字符串字段存储在单个字符数组中,然后使用具有偏移量的int字段,可以大大减少对象数量。 (即使在将任何数据放入其中之前,每个对象的开销大约为2个字。)

然后,您的属性可以转换为标准字符串。垃圾收集器很好地整理了很多短期垃圾,因此在访问属性时创建大量的“tmp”字符串应该不是问题。

(现在,如果很多字符串字段的值都没有变化,那么事情会变得容易多了)

答案 7 :(得分:1)

您可以通过使用一个存储字符的大字节[]然后将一个int-offset作为“字符串”保存到该数组中来保存每个对象的开销。

答案 8 :(得分:0)

也许一个好的旧时尚角色阵列可以满足您的需求。

答案 9 :(得分:0)

所有这些字符串都是不同的吗?

在大多数真实世界的数据集中,我不同的字符串的实际数量可能不会那么高,如果你考虑字符串实习,那么实际消耗的内存量可能会比你想象的要少得多。 / p>