Производительность фрагментации файлов в C#

Я пытаюсь дать пользователям возможность загружать большие файлы. Прежде чем загрузить файл, я хочу разбить его на части. Каждый фрагмент должен быть объектом C#. Причина в том, что для целей ведения журнала. Это длинная история, но мне нужно создать настоящие объекты C#, представляющие каждый фрагмент файла. Несмотря на это, я пробую следующий подход:

public static List<FileChunk> GetAllForFile(byte[] fileBytes)
{
  List<FileChunk> chunks = new List<FileChunk>();
  if (fileBytes.Length > 0)
  {
    FileChunk chunk = new FileChunk();
    for (int i = 0; i < (fileBytes.Length / 512); i++)
    {
      chunk.Number = (i + 1);
      chunk.Offset = (i * 512);
      chunk.Bytes = fileBytes.Skip(chunk.Offset).Take(512).ToArray();

      chunks.Add(chunk);
      chunk = new FileChunk();
    }
  }
  return chunks;
}

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

благодарю вас


person user609886    schedule 09.08.2012    source источник
comment
Вы определили, какая часть кода работает медленно?   -  person HABO    schedule 09.08.2012
comment
Насколько медленно это медленно? Как минимум, вы копируете все это. Вы можете использовать это в качестве основы, насколько больше, чем обычная копия, занимает ваш код? (Как в абсолютном времени, так и в процентах.) Также обратите внимание, что большие размеры блоков означают меньшие накладные расходы. Я бы также не стал жестко кодировать размер фрагмента, а использовал бы переменную/константу, чтобы вы могли изменить ее, если вам нужно.   -  person Servy    schedule 09.08.2012
comment
Весь подход здесь обречен на провал. У вас уже есть весь файл в памяти один раз (в массиве fileBytes). Теперь вы помещаете его в память снова, просто для того, чтобы разбить его на части. Почему? Не можете ли вы либо а) прочитать фрагмент с диска, обработать его, а затем перейти к следующему фрагменту, либо б) сделать так, чтобы фрагменты указывали на исходный массив со смещением и длиной?   -  person Chris Shain    schedule 09.08.2012
comment
Вы можете заменить Skip и Take на Array.Copy, чтобы избежать повторять fileBytes на каждом шаге.   -  person Paolo Moretti    schedule 09.08.2012
comment
Должен ли это быть список FileChunk? Вам нужно использовать массив байтов для ввода вместо потока? С большими / многими файлами, которые будут потреблять много памяти и могут вызвать подкачку.   -  person JamieSee    schedule 09.08.2012


Ответы (4)


Я подозреваю, что это будет немного больно:

chunk.Bytes = fileBytes.Skip(chunk.Offset).Take(512).ToArray();

Попробуйте это вместо этого:

byte buffer = new byte[512];
Buffer.BlockCopy(fileBytes, chunk.Offset, buffer, 0, 512);
chunk.Bytes = buffer;

(Код не проверен)

И причина, по которой этот код, вероятно, будет медленным, заключается в том, что Skip не делает ничего особенного для массивов (хотя мог бы). Это означает, что каждый проход в вашем цикле повторяет первые 512 * n элементов в массиве, что приводит к производительности O (n ^ 2), где вы должны просто видеть O (n).

person MarkPflug    schedule 09.08.2012
comment
+1 Кроме того, я бы инициализировал список фрагментов, указав начальную емкость, чтобы избежать перераспределения его внутреннего массива. - person Paolo Moretti; 09.08.2012
comment
@PaoloMoretti: да, инициализация коллекции до размера буфера или 512 была бы предпочтительнее! - person IAbstract; 10.08.2012

Попробуйте что-то вроде этого (непроверенный код):

public static List<FileChunk> GetAllForFile(string fileName, FileMode.Open)
{
  var chunks = new List<FileChunk>();
  using (FileStream stream = new FileStream(fileName))
  {
      int i = 0;
      while (stream.Position <= stream.Length)
      {
          var chunk = new FileChunk();
          chunk.Number = (i);
          chunk.Offset = (i * 512);
          Stream.Read(chunk.Bytes, 0, 512);
          chunks.Add(chunk);
          i++;
      }
  }
  return chunks;
}

Приведенный выше код пропускает несколько шагов в вашем процессе, предпочитая читать байты из файла напрямую.

Обратите внимание, что если размер файла не кратен четному числу 512, последний фрагмент будет содержать менее 512 байт.

person Robert Harvey    schedule 09.08.2012
comment
это, вероятно, нормально (также непроверенный комментарий), за исключением того, что вам нужно добавить смещение в качестве второго параметра в Stream.read - person Alex; 09.08.2012
comment
@Alex: Нет, если OP хочет, чтобы данные копировались в массив фрагментов, начиная с начала массива, что подразумевает его исходный код. - person Robert Harvey; 09.08.2012
comment
Я думаю, что он прав, если вы укажете смещение 0 каждый раз, вы будете получать те же 512 байт при каждом чтении. смещение должно быть 0 для первого чтения, 512 для следующего и т. д. - person Kevin; 09.08.2012

То же, что и ответ Роберта Харви, но с использованием BinaryReader, поэтому мне не нужно указывать смещение. Если вы используете BinaryWriter на другом конце для повторной сборки файла, вам не понадобится элемент Offset FileChunk.

public static List<FileChunk> GetAllForFile(string fileName) {
    var chunks = new List<FileChunk>();
    using (FileStream stream = new FileStream(fileName)) {
        BinaryReader reader = new BinaryReader(stream);
        int i = 0;
        bool eof = false;
        while (!eof) {
            var chunk = new FileChunk();
            chunk.Number = i;
            chunk.Offset = (i * 512);
            chunk.Bytes = reader.ReadBytes(512);
            chunks.Add(chunk);
            i++;
            if (chunk.Bytes.Length < 512) { eof = true; }
        }
    }
    return chunks;
}

Думали ли вы о том, что вы собираетесь делать, чтобы компенсировать потерю пакетов и повреждение данных?

person Kevin    schedule 09.08.2012

Поскольку вы упомянули, что загрузка занимает много времени, я бы использовал асинхронное чтение файлов, чтобы ускорить процесс загрузки. Жесткий диск — самый медленный компонент компьютера. Google выполняет асинхронное чтение и запись в Google Chrome, чтобы сократить время загрузки. Мне приходилось делать что-то подобное на C# на предыдущей работе.

Идея заключалась бы в том, чтобы создать несколько асинхронных запросов к разным частям файла. Затем, когда приходит запрос, возьмите массив байтов и создайте свои объекты FileChunk, занимающие 512 байтов за раз. В этом есть несколько преимуществ:

  1. Если вы запускаете этот процесс в отдельном потоке, то вся программа не будет ожидать загрузки большого файла, который у вас есть.
  2. Вы можете обрабатывать массив байтов, создавая объекты FileChunk, в то время как жесткий диск все еще пытается заполнить запрос на чтение других частей файла.
  3. Вы сэкономите место в оперативной памяти, если ограничите количество ожидающих запросов на чтение, которые вы можете иметь. Это позволяет уменьшить количество ошибок страниц на жестком диске и более эффективно использовать ОЗУ и кэш-память ЦП, что еще больше ускоряет обработку.

Вы хотели бы использовать следующие методы в классе FileStream.

[HostProtectionAttribute(SecurityAction.LinkDemand, ExternalThreading = true)]
public virtual IAsyncResult BeginRead(
    byte[] buffer,
    int offset,
    int count,
    AsyncCallback callback,
    Object state
)

public virtual int EndRead(
    IAsyncResult asyncResult
)

Также это то, что вы получите в asyncResult:

// Extract the FileStream (state) out of the IAsyncResult object
FileStream fs = (FileStream) ar.AsyncState;

// Get the result
Int32 bytesRead = fs.EndRead(ar);

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

Это пример кода для работы с Модели асинхронного файлового ввода-вывода.

Это ссылка на документацию MS для Асинхронный файловый ввод/вывод.

person Flynn Jones    schedule 09.08.2012
comment
Маловероятно, что ОП ждет на жестком диске, тем более что его исходный код даже не читает из файловой системы. Более вероятно, что виновниками являются сложность O(N^2) его операций Skip/Take и выделение ненужных повторяющихся структур данных. - person Robert Harvey; 10.08.2012