我其实也遇到了类似的问题,但是参数有些不同。我的应用程序处理两种类型的字符串 - 相对较短的字符串长度为60-100个字符,和相对较长的字符串长度在100-1000个字节之间(平均约为300)。
我的用例还必须支持Unicode文本,但实际上只有一小部分字符串包含非英语字符。
在我的用例中,我将每个String属性公开为本地String,但底层数据结构是一个byte[],保存了Unicode字节。
我的用例还需要搜索和排序这些字符串,获取子字符串和其他常见字符串操作。我的数据集有数百万条记录。
基本实现看起来像这样:
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)。即使您的字符串可以更长,则int仍然比byte[]的12-16字节开销更便宜。
这样很大程度上减少了byte[]的开销,并增加了一些复杂性但不会影响性能(编码传递相对于涉及数组复制而言相对昂贵)。
此实现大致如下:
byte _property1;
byte _property2;
byte _proeprty3;
private byte[] _data;
byte[] data;
private int GetStartIndex(int propertyIndex)
{
int result = 0;
switch(propertyIndex)
{
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);}
}
设置器的作用类似于-将原始数据复制到两个数组中(从0开始到startIndex,以及从startIndex + length到length之间),然后使用三个数组(dataAtStart + NewData + EndData)创建一个新数组,并将数组的长度设置为适当的局部变量。
我仍然不满意节省的内存和每个属性的手动实现所需的非常艰苦的劳动,因此我构建了一个内存压缩分页系统,使用极快的QuickLZ来压缩完整页面。这使我对时间-内存折衷(本质上是页面大小)有了很多控制。
与更有效的byte[]存储相比,我的用例的压缩率接近50%(!)。我使用大约每页10个字符串的页面大小,并将相似的属性分组在一起(倾向于具有类似数据的属性)。
这增加了额外的10-20%开销(除了仍然需要的编码/解码过程)。分页机制会缓存最近访问的页面,最多可配置大小。
即使没有压缩,该实现也允许您为每个页面设置固定的开销因子。
当前页面缓存实现的主要缺点是,在进行压缩时它不是线程安全的(如果没有压缩,则不存在此问题)。
如果您对压缩的分页机制感兴趣,请让我知道(我一直在寻找开源的借口)。