String VS Byte [], использование памяти

У меня есть приложение, которое использует большое количество строк. Поэтому у меня есть проблема с использованием памяти. Я знаю, что одним из лучших решений в этом случае является использование БД, но я не могу использовать это на данный момент, поэтому я ищу другие решения.

В С# строка хранится в 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[]

Здесь строки занимают гораздо меньше места памяти, чем байт. Это может быть удивительно, но я полагал, что пустая строка ссылается только один раз. Это? Но я не знаю, может ли это объяснить всю эту огромную разницу. Это какая-то другая причина? Какое наилучшее решение?

Ответ 1

Это может быть удивительно, но я предположил, что пустая строка ссылается только один раз.

Да, пустой StringBuilder возвращает string.Empty в качестве результата. Ниже приведен фрагмент кода True:

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

Но я не знаю, может ли это объяснить всю эту огромную разницу.

Да, это прекрасно объясняет это. Вы сохраняете 5000 объектов string. Разница в байтах составляет примерно 270 000 (198 000/2), поэтому около 170 кбайт. Разделение на 5 вы получаете 34 байта на объект, что примерно соответствует размеру указателя на 32-битной системе.

Какое наилучшее решение?

Сделайте то же самое: сделайте себя private static readonly пустым массивом и используйте его каждый раз, когда вы получите string.Empty от sb.ToString():

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