Лучший/быстрый способ заполнить большой массив в С#

У меня есть 3 *.dat файлы (346 КБ, 725 КБ, 1762 КБ), которые заполняются json-строкой из "больших" массивов int-Arrays.

Каждый раз, когда мой объект создается (несколько раз), я беру эти три файла и использую JsonConvert.DeserializeObject для десериализации массивов в объект.

Я думал об использовании двоичных файлов вместо json-строки или могу ли я даже сохранить эти массивы напрямую? Мне не нужно использовать эти файлы, это просто местоположение, в котором данные сохраняются. Я бы с радостью переключился на что-нибудь быстрее.

Каковы различные способы ускорения инициализации этих объектов?

Ответ 1

Самый быстрый способ состоит в ручной сериализации данных.

Легкий способ сделать это - создать FileStream, а затем обернуть его в BinaryWriter/BinaryReader.

У вас есть доступ к функциям для записи базовых структур данных (numbers, string, char, byte[] и char[]).

Легкий способ записи int[] (ненужный, если фиксированный размер) заключается в добавлении длины массива с использованием int/long (в зависимости от размера unsigned не дает никаких преимуществ, поскольку массивы использовать подписанные типы данных для хранения их длины). А затем напишите все ints.

Два способа записать все ints:
1. Просто перейдите по всему массиву.
2. Преобразуйте его в byte[] и напишите его с помощью BinaryWriter.Write(byte[])

Вот как вы можете реализовать их оба:

// Writing
BinaryWriter writer = new BinaryWriter(new FileStream(...));
int[] intArr = new int[1000];

writer.Write(intArr.Length);
for (int i = 0; i < intArr.Length; i++)
    writer.Write(intArr[i]);

// Reading
BinaryReader reader = new BinaryReader(new FileStream(...));
int[] intArr = new int[reader.ReadInt32()];

for (int i = 0; i < intArr.Length; i++)
    intArr[i] = reader.ReadInt32();

// Writing, method 2
BinaryWriter writer = new BinaryWriter(new FileStream(...));
int[] intArr = new int[1000];
byte[] byteArr = new byte[intArr.Length * sizeof(int)];
Buffer.BlockCopy(intArr, 0, byteArr, 0, intArr.Length * sizeof(int));

writer.Write(intArr.Length);
writer.Write(byteArr);

// Reading, method 2
BinaryReader reader = new BinaryReader(new FileStream(...));
int[] intArr = new int[reader.ReadInt32()];
byte[] byteArr = reader.ReadBytes(intArr.Length * sizeof(int));
Buffer.BlockCopy(byteArr, 0, intArr, 0, byteArr.Length);

Я решил поставить все это на тест, с массивом из 10000 целых я провела тест 10000 раз.

Это привело к тому, что один метод потребляет в моей системе в среднем 888200ns (около 0,89 мс).
Хотя метод 2 потребляет в среднем только 568600ns в моей системе (0,57 мс в среднем).

Оба раза включают работу, которую должен сделать сборщик мусора.

Очевидно, что метод 2 быстрее, чем метод 1, хотя, возможно, менее читаемый.

Другая причина, по которой метод 1 может быть лучше, чем метод 2, заключается в том, что для метода 2 требуется вдвое больше объема оперативной памяти, чем данные, которые вы собираетесь писать (исходные int[] и byte[], которые были преобразованы из int[]) при работе с ограниченным ОЗУ/чрезвычайно большими файлами (речь идет о 512 Мбайт +), хотя, если это так, вы всегда можете сделать гибридное решение, например, записывая 128 МБ за раз.

Обратите внимание, что для метода 1 также требуется дополнительное пространство, но поскольку он разбивается на 1 операцию на элемент int[], он может освободить память намного раньше.

Что-то вроде этого будет писать 128 Мбайт int[] за раз:

const int WRITECOUNT = 32 * 1024 * 1024; // 32 * sizeof(int)MB

int[] intArr = new int[140 * 1024 * 1024]; // 140 * sizeof(int)MB
for (int i = 0; i < intArr.Length; i++)
    intArr[i] = i;

byte[] byteArr = new byte[WRITECOUNT * sizeof(int)]; // 128MB

int dataDone = 0;

using (Stream fileStream = new FileStream("data.dat", FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fileStream))
{
    while (dataDone < intArr.Length)
    {
        int dataToWrite = intArr.Length - dataDone;
        if (dataToWrite > WRITECOUNT) dataToWrite = WRITECOUNT;
        Buffer.BlockCopy(intArr, dataDone, byteArr, 0, dataToWrite * sizeof(int));
        writer.Write(byteArr);
        dataDone += dataToWrite;
    }
}

Обратите внимание, что это только для записи, чтение работает по-разному: P. Надеюсь, это даст вам более глубокое представление о работе с очень большими файлами данных:).

Ответ 2

Если у вас только куча целых чисел, то использование JSON действительно будет довольно неэффективным с точки зрения разбора. Вы можете использовать BinaryReader и BinaryWriter для эффективной работы с бинарными файлами... но мне непонятно, почему вам нужно читать файл каждый раз, когда вы создаете объект в любом случае. Почему каждый новый объект не может ссылаться на исходный массив, который был прочитан один раз? Или, если им нужно мутировать данные, вы можете сохранить один "канонический источник" и просто копировать этот массив в памяти каждый раз, когда вы создаете объект.

Ответ 3

Самый быстрый способ создать массив байтов из массива целых чисел - использовать Buffer.BlockCopy

byte[] result = new byte[a.Length * sizeof(int)];
Buffer.BlockCopy(a, 0, result, 0, result.Length);
// write result to FileStream or wherever

Если вы сохраняете размер массива в первом элементе, вы можете использовать его снова для десериализации. Убедитесь, что все вписывается в память, но, глядя на ваши размеры файлов, он должен.

var buffer = File.ReadAllBytes(@"...");
int size = BitConverter.ToInt32(buffer,0);
var result = new int[size];
Buffer.BlockCopy(buffer, 0, result, result.length);

Двоичный файл не читается человеком, но определенно быстрее, чем JSON.