【问题标题】:How to implement string with 1 byte char (and save memory)如何用 1 字节字符实现字符串(并节省内存)
【发布时间】:2013-04-18 19:23:17
【问题描述】:

如何实现基于单字节的字符串?

应用程序使用大量单词。
单词来自 SQL,是 varchar(单字节)。
每个单词也有 Int32 ID。
将单词下载到:

Dictionionary<Int32,string> 

为了性能。

问题是字典太大,会出现内存不足异常。
我们最终会拆分数据。
该应用程序如此频繁地访问列表,以至于无法为每个请求访问 SQL。
数据库已经非常活跃。
动态分页进出字典不是一个选项 - 它绑定到 ListView 并且使用虚拟化效果很好。
单词只在晚上加载 - 用户只需要一个静态列表。
他们使用这些词来搜索和处理其他数据,但他们不处理这些词。

既然是 char 就可以实现一个基于单字节的单词:

public class StringByte1252 : Object, IComparable, IComparable<StringByte1252>
{
    static Encoding win1252 = Encoding.GetEncoding("Windows-1252");

    public Int32 ID { get; private set; }
    public byte[] Bytes { get; private set; }

    public string Value { get { return win1252.GetString(Bytes); } }
    public Int32 Length { get { return Bytes.Length; } }

    public int CompareTo(object obj)
    {
        if (obj == null)
        {
            return 1;
        }
        StringByte1252 other = obj as StringByte1252;
        if (other == null)
        {
            throw new ArgumentException("A StringByte1252 object is required for comparison.", "obj");
        }
        return this.CompareTo(other);
    }
    public int CompareTo(StringByte1252 other)
    {
        if (object.ReferenceEquals(other, null))
        {
            return 1;
        }
        return string.Compare(this.Value, other.Value, StringComparison.OrdinalIgnoreCase);
    }
    public override bool Equals(Object obj)
    {
        //Check for null and compare run-time types.
        if (obj == null || !(obj is StringByte1252)) return false;
        StringByte1252 item = (StringByte1252)obj;
        return (this.Bytes == item.Bytes);
    }
    public override int GetHashCode() { return ID; }

    public StringByte1252(Int32 id, byte[] bytes) { ID = id; Bytes = bytes; } 
}

上面的方法有效,但它并不比内存效率更高

Dictionionary<Int32,string>

基于 Int16 字符的字典实际上使用的内存略少。

我哪里做错了?
字节数组占用的空间是否超过字节总和?
有没有办法实现单字节字符串?

【问题讨论】:

  • 如果你使用List&lt;byte&gt;而不是String会发生什么?
  • @DanPichelman 我该如何列出
  • 单词的平均长度是多少?您的代码为每个字符串分配两个对象,而字符串只分配一个。因此,您的代码只会在长字符串中领先。
  • Jon Skeet 有一篇文章全面讨论了这些确切问题:Of memory and strings
  • 长度为 8 时,每个对象的开销和每个字典条目的开销都大于实际的字符数据。

标签: .net string character-encoding


【解决方案1】:

一个数组在 64 位运行时大约有 50 字节的开销。在 32 位运行时,它会少一点:可能是 40 字节。有标准的 .NET 分配开销(64 位运行时为 24 个字节),然后是数组的所有元数据:维数、长度等。您无法通过使用单个字节数组存储短来节省内存字符串。

一种方法是分配一个非常大的字节数组并将字符串存储在该数组中,UTF-8 编码。你的字典变成了Dictionary&lt;int,int&gt;Value 是数组的索引。

我在我的文章 Reducing Memory Required for Strings 中展示了如何做到这一点。通过这种方式,我能够比正常的字符串分配节省大约 50%。详情请参阅文章。

另一个问题是Dictionary 开销类似于每个条目 24 个字节。如果你有一大堆小物件,那是相当昂贵的。您可以考虑改为制作结构列表,按 ID 对其进行排序,并使用二进制搜索。 Dictionary 给你的不是 O(1) 访问,但对于用户界面来说,它可能足够快。那么您的开销将是每个条目 8 个字节。

结构类似于:

struct WordEntry
{
    public readonly int Id;
    public readonly int IndexIntoStringTable;
}

【讨论】:

  • 这就是我对许多很少更改的短字符串采取的方法。所以它似乎非常适合 OPs 问题。
  • 我关注,但我需要绑定到一个集合,该集合公开一个生成字符串的公共属性。
  • 跟随结构。应用程序会进行很多字典查找,但它们一次只能查找 200 个,所以如果它减少了内存,我会试一试。
  • @Blam:您可以编写一个包含列表(或字典)和字符串池的包装器对象。然后 indexer 属性进行查找。
  • @JimMischel 鉴于单词列表在使用中是只读的,是否可以通过插入字符串而不是堆分配来节省空间?
【解决方案2】:

尽管 char 是字节大小的两倍,但如果字符串很长,您只会在内存占用方面获得显着差异。

内存以块的形式分配,例如 16 字节(可能因平台和实现而异)。这意味着一个字符长的字符串可能会占用与六个字符长的字符串一样多的内存,因为两者都需要两个内存块来保存字符数据和字符串对象的开销。

由于字典中引用的开销、字符串对象中的开销以及部分未使用的内存块的开销,您需要在字符串中平均放入大约 16 个字符,然后您的开销才会低于 50%。

由于开销如此之大,仅通过减小数据大小来减少内存占用是很困难的。

您可能会寻找一种解决方案,让每个项目的开销更少,例如一个用于字符数据的巨型字符串(或字节数组),并为该大字符串中的每个字符串指定起始索引。

【讨论】:

  • 平均长度为 8。最小值为 3,最大值为 600(很少有大的)。未来的最大尺寸为 40。
  • @Blam:平均长度意味着在将它们存储为字符串时会有大约 70% 的开销。
【解决方案3】:

由于数组的构建方式,字节数组肯定占用与其内容总和更多/一样多的空间。数组由特定大小的块组成。确实能够区分数组的不同元素,元素必须有固定的大小,所以数字不会混淆。这就像说您要存储最多 9999 的十进制数,因此为了能够将它们“一起”存储,您必须使用前导零来填补空白:1,5,32,1293,12 = 00010005003212930012。 一个词是由字符组成的。为了能够表示一个字符,您需要找到可能使用的最少字符数,并从中定义数组的基本构造单元。 由于字母表中有 26 个字符,大写字母和小写字母加起来是 52 个,而与其他符号一起,您可能会得到少于 128 个的可能性,导致您选择 7 位。内存由 8 位块(字节)组成,因此您应该解决这些问题并使用 ASCII 编码,或者找到一种方法来处理数据,以便每个字符仅使用 7 位,并且每第 8 个字符保存一个字节。我想有一些开放的解决方案,虽然我不知道。

字符串只是一个字符数组。在 c 中,字符串是 byte[]。 尝试使用 byte[] 数组。

由于我目前正在运行 linux,我发现很难测试,但您也可以尝试使用:

byte[] bytes = Encoding.ASCII.GetBytes("Hello World!");

【讨论】:

  • Encoding.GetEncoding("Windows-1252");使用一个完整的字节。一个完整的字节是我从 SQL 中得到的,这就是我想要的。我得到一个数组有开销但有 2:1 的开销?单词的平均长度为 8。
  • 是的,它实际上很奇怪。对不起,我想我没有答案。
  • 在 .NET 中,字符串由 char 组成,而 char 是 Int16 而不是字节。我看不到从 UNICODE 字符串中获取 ASCII 字节对我有什么帮助。
  • 是的,我现在明白了。但是,您可以使用字节数组来代替 char 数组。字节定义为一个字节,而 ASCII 是一个字节大小。 stackoverflow.com/questions/2134002/…
【解决方案4】:

得出的结论是,简单的结构是将 char 存储为单字节的最佳内存执行器。

这是我的假设。
内存一次分配 4 个字节。
单个变量末尾的字节或不在 4 字节边界上的数组都被浪费了。

字节池消除了单个单词上浪费的字节,但代价是对池起点和字长的索引。

假设即使是字典 Int32,字符串也有浪费。
任何奇数长度的字符串都会浪费 16 个字节。

考虑 Int32 词索引。
int32浪费了1个字节。
在池的情况下,池索引只有 Int32,所以很明显它不能保存 Int32 字。
对于 .NET 中的对象大小,有 4 GB 的限制。
单词加索引的最佳情况是 8 个字节。
32(4GB) - 8 = 24
最大单词加索引计数为 2 ** 24 = 16,777,216。

此结构使用索引中的一个字节作为一个字符。
一个字节就是一个字节。 该结构不必存储长度,因为它可以从内容中导出长度。

public struct Word1252bytes 
{
    static Encoding win1252 = Encoding.GetEncoding("Windows-1252");
    private UInt64 packed;
    private byte[] bytes;
    public Int32 Key { get { return (Int32)(packed & ((1 << 24) - 1)); } }
    private byte[] Bytes
    {
        get
        {
            // yes a lot of work to salage just one byte out of the key 
            // but a byte is a byte and the design objective is size
            byte[] bytesT = new byte[Length];
            bytesT[0] = (byte)((packed >> 24) & ((1 << 8) - 1));
            for (int i = 0; i < bytes.Length; i++) bytesT[i + 1] = bytes[i];
            return bytesT;
        }
    }
    public Int32 Length { get { return bytes.Length + 1; } }
    public String Value
    {
        get
        {
            return win1252.GetString(Bytes);
        }
    }
    public Word1252bytes(UInt64 Packed, byte[] Bytes) 
    { 
        packed = Packed;
        bytes = Bytes;
    }
}

如何打包

pack32 = (UInt32)(bits24wordI) | ((UInt32)charB[0] << 24);
byte[] bytesT = new byte[bits8wLen - 1];
for (int i = 1; i < bits8wLen; i++) bytesT[i - 1] = charB[i];
iWordsList.Add(new Word1252bytes(pack32, bytesT));

补水快。我的测试用例是 600 万字,Word1252bytes 的构建速度和字典一样快(大约 20 秒)

大小比较。
带字节池可以使用一个字节的字索引作为字长。
这将字长限制为 256,但在字符串池中节省了空间。

上述结构在每个单词大小上都与所述假设相关。 索引是索引的大小。 对于 Dict 和上述结构,它是 32。
对于字节池,它是 64 - 单词索引和池索引。
浪费是不在 4 字节边界上的字节。

        Index   Content Waste   Total
exactly 1               
dict16  dict16  32  16      16  64
word8   pool    64  8           72
word8   imbed   32  0       0   32

exactly 2               
dict16  dict16  32  32      0   64
word8   pool    64  16          80
word8   imbed   32  8       24  64

exactly 3               
dict16  dict16  32  48      16  96
word8   pool    64  24          88
word8   imbed   32  16      16  64

exactly 4               
dict16  dict16  32  64      0   96
word8   pool    64  32          96
word8   imbed   32  24      8   64

exactly 5               
dict16  dict16  32  80      16  128
word8   pool    64  40          104
word8   imbed   32  32  0   64

exactly 6               
dict 16 dict16  32  96      0   128
word 8  pool    64  48          112
word 8  imbed   32  40      24  96

exactly 7               
dict 16 dict16  32  112     16  160
word 8  pool    64  56          120
word 8  imbed   32  48      16  96

exactly 8               
dict 16 dict16  32  128     0   160
word 8  pool    64  64          128
word 8  imbed   32  56      8   96

exactly 9               
dict 16 dict16  32  144     16  192
word 8  pool    64  72          136
word 8  imbed   32  64      0   96

exactly 10              
dict 16 dict16  32  160      0  192
word 8  pool    64  80          144
word 8  imbed   32  72       24 128

exactly 254             
dict 16 dict16  32  4064    0   4096
word 8  pool    64  2032        2096
word 8  imbed   32  2024    24  2080

exactly 255             
dict 16 dict16  32  4080    16  4128
word 8  pool    64  2040        2104
word 8  imbed   32  2032    16  2080

exactly 256             
dict 16 dict16  32  4096    0   4128
word 8  pool    64  2048        2112
word 8  imbed   32  2040    8   2080

exactly 257             
dict 16 dict16  32  4112    16  4160
word 8  pool    64  2056        2120
word 8  imbed   32  2048    0   2080

字节池可以压缩的地方是搜索池中已经存在的字符串。在小字符串上,池处于 30% 的劣势,因此它需要有很高的匹配率。在较大的字符串上,找到匹配项的机会很低。问题是搜索时间。在超过 1,000,000 的列表中,即使 10 毫秒的搜索时间也是 10,000 秒 = 2.78 小时。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-22
    • 1970-01-01
    • 2019-09-18
    • 1970-01-01
    相关资源
    最近更新 更多