字符串 VS 字节数组,内存使用情况

12

我有一个应用程序使用了大量字符串,因此我遇到了一些内存使用问题。我知道在这种情况下最好的解决方案之一是使用数据库,但目前我无法使用它,所以我正在寻找其他解决方案。

在C#中,字符串存储为Utf16,这意味着与Utf8相比我损失了一半的内存使用率(对于我的大部分字符串而言)。因此,我决定使用utf8字符串的字节数组。但出乎我的意料,这个解决方案所需的内存空间比我的应用程序中简单字符串的两倍还多。

因此,我进行了一些简单的测试,但我想知道专家们的意见以确保我没有做错。

测试1:分配固定长度的字符串

var stringArray = new string[10000];
var byteArray = new byte[10000][];
var Sb = new StringBuilder();
var utf8 = Encoding.UTF8;
var stringGen = new Random(561651);
for (int i = 0; i < 10000; i++) {
    for (int j = 0; j < 10000; j++) {
        Sb.Append((stringGen.Next(90)+32).ToString());
    }
    stringArray[i] = Sb.ToString();
    byteArray[i] = utf8.GetBytes(Sb.ToString());
    Sb.Clear();
}
GC.Collect();
GC.WaitForFullGCComplete(5000);

内存使用

00007ffac200a510        1        80032 System.Byte[][]
00007ffac1fd02b8       56       152400 System.Object[]
000000bf7655fcf0      303      3933750      Free
00007ffac1fd5738    10004    224695091 System.Byte[]
00007ffac1fcfc40    10476    449178396 System.String

正如我们所看到的,字节数组占用的内存空间是普通数组的一半,这并不出乎意料。

测试2:随机大小的字符串分配(具有实际长度)

var stringArray = new string[10000];
var byteArray = new byte[10000][];
var Sb = new StringBuilder();
var utf8 = Encoding.UTF8;
var lengthGen = new Random(2138784);
for (int i = 0; i < 10000; i++) {
    for (int j = 0; j < lengthGen.Next(100); j++) {
        Sb.Append(i.ToString());
        stringArray[i] = Sb.ToString();
        byteArray[i] = utf8.GetBytes(Sb.ToString());
    }
    Sb.Clear();
}
GC.Collect();
GC.WaitForFullGCComplete(5000);

内存使用情况

00007ffac200a510        1        80032 System.Byte[][]
000000be2aa8fd40       12        82784      Free
00007ffac1fd02b8       56       152400 System.Object[]
00007ffac1fd5738     9896       682260 System.Byte[]
00007ffac1fcfc40    10368      1155110 System.String

字符串所占用的空间比字节数组的空间少一点,大约是两倍的时间。当字符串更短时,我期望会有更多的开销。 但事实上相反,为什么呢?

测试3: 与我的应用程序相对应的字符串模型

var stringArray = new string[10000];
var byteArray = new byte[10000][];
var Sb = new StringBuilder();
var utf8 = Encoding.UTF8;
var lengthGen = new Random();
for (int i=0; i < 10000; i++) {
    if (i%2 == 0) {
        for (int j = 0; j < lengthGen.Next(100000); j++) {
            Sb.Append(i.ToString());
            stringArray[i] = Sb.ToString();
            byteArray[i] = utf8.GetBytes(Sb.ToString());
            Sb.Clear();
        }
    } else {
        stringArray[i] = Sb.ToString();
        byteArray[i] = utf8.GetBytes(Sb.ToString());
        Sb.Clear();
    }
}
GC.Collect();
GC.WaitForFullGCComplete(5000);

内存使用情况

00007ffac200a510        1        80032 System.Byte[][]
00007ffac1fd02b8       56       152400 System.Object[]
00007ffac1fcfc40     5476       198364 System.String
00007ffac1fd5738    10004       270075 System.Byte[]

使用“Here strings”比使用字节占用更少的内存空间。这可能令人惊讶,但我认为空字符串只被引用一次。是吗?但我不知道这是否能解释所有巨大的差异。还有其他原因吗?最好的解决方案是什么?

1个回答

5

这可能会令人惊讶,但我认为空字符串只被引用一次。

是的,一个空的 StringBuilder 返回 string.Empty 作为其结果。下面的代码片段打印出 True:

var sb = new StringBuilder();
Console.WriteLine(object.ReferenceEquals(sb.ToString(), string.Empty));

但我不确定这是否能解释所有巨大的差异。
是的,这完全解释了它。你可以节省5000个字符串对象。字节上的差异大约为270,000-(198,000/2),因此大约为170 kBytes。将其除以5,每个对象约为34个字节,这大致相当于32位系统上指针的大小。
什么是最佳解决方案?
做同样的事情:创建一个私有的静态只读空数组,并在从sb.ToString()获得string.Empty时使用它。
private static readonly EmptyBytes = new byte[0];
...
else
{
    stringArray[i] = Sb.ToString();
    if (stringArray[i] == string.Empty) {
        byteArray[i] = EmptyBytes;
    } else {
        byteArray[i] = utf8.GetBytes(Sb.ToString());
    }
    Sb.Clear();
}

为什么不使用 String.IsNullOrEmpty(stringArray[i]) - Mark Jansen
@MarkJansen 这只是一个例子:我确定在 if (i%2 == 0) 条件的 else 分支中,stringArray[i] 是空的,所以我本来可以完全跳过与 string.Empty 的比较。 - Sergey Kalinichenko
确实有趣,使用空字节引用可以大大改善内存使用情况。我在帖子中忘记说了,我是在64位下,单位是字节。无论如何,这并不改变你解释的想法,即使我发现一个指针需要34个字节(每个字符串还需要26个字节的开销),这也太多了。我已经失去了10K指针的大小(80032 KB,即可用内存大小的25%)。 有没有办法避免使用这么多间接引用?也许不能用字节数组。 - Edeen
@Edeen,如果你的字符串不需要改变,你可以通过自己编写内存“分配器”来将更多的数据压缩到相同数量的字节中。基本上,你可以在一开始就分配所有的字节,然后将其分成字符串,并存储下一个字符串开始的索引。但是,如果你需要用新的长度不同的字符串替换字符串,则无法使用此方法。 - Sergey Kalinichenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接