Algorytm na defragmentację dużego pliku

Uwaga! Informacje na tej stronie mają ponad 5 lat. Nadal je udostępniam, ale prawdopodobnie nie odzwierciedlają one mojej aktualnej wiedzy ani przekonań.

# Algorytm na defragmentację dużego pliku

Sun
15
Feb 2009

SceNtriC na swoim blogu często pisze długie notki, w których opisuje swoje algorytmy, więc ja nie będę gorszy i dzisiaj też tak zrobię :) Problem, który rozwiązywałem niedawno, wyglądał tak: Mamy duży plik - tak duży, że nie chcemy wczytywać go całego do pamięci. W pliku są bloki z danymi, a między nimi dziury (na rysunku zakreskowane). Zadaniem jest tak "zsunąć" te dane, żeby dziur nie było.

Defragmentacja bloków danych

Dziury można opisać tablicą takich struktur, z których każda pamięta numer pierwszego bajtu i liczbę bajtów:

struct BLOCK
{
  unsigned Start, Size;
  
  BLOCK() { }
  BLOCK(unsigned4 Start, unsigned4 Size) : Start(Start), Size(Size) { }
  bool operator < (const BLOCK &b) const { return Start < b.Start; }
};

std::vector<BLOCK> BlockToDelete;

Te puste bloki warto najpierw posortować rosnąco według ich początku. Oczywiście zakładamy, że nie nachodzą na siebie.

std::sort(BlocksToDelete.begin(), BlocksToDelete.end());

Warto je po tym przetworzyć w taki sposób, żeby sąsiadujące ze sobą puste bloki (jak dwa pierwsze na rysunku) połączyć w jeden.

void MergeBlocksToDelete()
{
  if (BlocksToDelete.size() < 2) return;
  unsigned i = BlocksToDelete.size()-1;
  while (i > 0)
  {
    // Próbuje połączyć blok z poprzednim.
    if (BlocksToDelete[i].Start == BlocksToDelete[i-1].Start + BlocksToDelete[i-1].Size)
    {
      BlocksToDelete[i-1].Size += BlocksToDelete[i].Size;
      BlocksToDelete.erase(BlocksToDelete.begin() + i);
    }
    i--;
  }
}

Czas na właściwą defragmentację pliku. Nie chcemy wczytywać całego pliku do pamięci, więc będziemy dane przepisywać przez pewien bufor, tak jak pokazują strzałki na rysunku. Całe przepisywanie odbywa się w miejscu, bez użycia dodatkowego miejsca na dysku czy dodatkowego pliku. Przepisywanie zaczynamy od miejsca, gdzie kończy się pierwszy pusty blok. Pamiętamy zawsze aktualne miejsca z którego przepisujemy i aktualne miejsce, do którego przepisujemy. Jeden przebieg pętli głównej przepisuje dane pomiędzy dwoma pustymi blokami lub za ostatnim takim blokiem.

int ApplyBlocksToDeleteToFileContents()
{
  if (BlocksToDelete.empty()) return 0;

  const unsigned BUF_SIZE = 1024 * 1024; // 1 MB
  std::vector<char> Buf(BUF_SIZE);

  unsigned DstOffset = BlocksToDelete[0].Start;
  for (unsigned Block_i = 0; Block_i < BlocksToDelete.size(); Block_i++)
  {
    unsigned SrcOffset = BlocksToDelete[Block_i].Start + BlocksToDelete[Block_i].Size;
    unsigned SizeToMove;
    if (Block_i == BlocksToDelete.size()-1)
      SizeToMove = FileSize - SrcOffset;
    else
      SizeToMove = BlocksToDelete[Block_i+1].Start - SrcOffset;

    MoveFileData(
      DstOffset, SrcOffset, SizeToMove, &Buf[0], BUF_SIZE);
    DstOffset += SizeToMove;
  }

  for (unsigned Block_i = 0; Block_i < BlocksToDelete.size(); Block_i++)
    FileSize -= BlocksToDelete[Block_i].Size;
}

Funkcja pomocnicza przepisuje dane w pliku z jednego miejsca w drugie przez podany bufor. Jej dokładna postać zależy od tego, jakich funkcji używamy do obsługi plików.

int MoveFileData(
  unsigned DstOffset, unsigned SrcOffset, unsigned Size,
  char *Buf, unsigned BufSize)
{
  unsigned CurrSize;
  DWORD CurrTime;
  while (Size > 0)
  {
    CurrSize = Size;
    if (CurrSize > BufSize) CurrSize = BufSize;
    
    File->SetPos(SrcOffset);
    File->Read(Buf, CurrSize);

    File->SetPos(DstOffset);
    File->Write(Buf, CurrSize);

    DstOffset += CurrSize;
    SrcOffset += CurrSize;
    Size -= CurrSize;
  }

  return 0;
}

Zostaje jeszcze przyciąć plik do nowej długości. Może nie wszyscy wiedzą, ale w bibliotekach do obsługi plików zawsze istnieje taka funkcja, która przycina plik do miejsca w którym jest aktualnie kursor, a tym samym ustawia nowy rozmiar pliku i porzuca dane, które zostały dalej. Np. w WinAPI to będzie tak:

SetFilePointer(FileHandle, (LONG)FileSize, NULL, FILE_BEGIN);
SetEndOfFile(FileHandle);

Ponieważ dane w pliku mają jakieś znaczenie i pewnie gdzieś zapamiętane są offsety do nich, trzeba też uaktualnić te offsety, żeby pokazywały na nowe pozycje danych.

void UpdateDataOffsets()
{
  for (unsigned Record_i = 0; Record_i < RecordCount; Record_i++)
  {
    uint OffsetDiff = 0;
    for (uint i = 0; i < BlockCount; i++)
    {
      if (BlocksToDelete[i].Start < Records[Record_i].ContentOffset)
        OffsetDiff += BlocksToDelete[i].Size;
      else
        break;
    }
    Records[Record_i].ContentOffset -= OffsetDiff;
  }
}

Comments | #algorithms Share

Comments

STAT NO AD
[Stat] [STAT NO AD] [Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2019