Tekninen artikkeli

IO-suorituskyvyn (IO Performance) optimointi gigatavuluokan PDF-käsittelyssä

PDF-jäsentimen ensimmäinen hyödyllinen luku tapahtuu tiedoston väärässä päässä Formaatti sijoittaa startxref-osoittimen viimeisiin tavuihin, joten 1,8 Gt:n arkiston käsittely alkaa siirtymisellä tiedoston loppuun, yhden kilotavun lukemisella ja sitten hypyllä sinne, missä ristiviiteosataulukko sanoo asiakirjaluettelon sijaitsevan Siitä eteenpäin jäsennys on satunnaiskävely koko tavualueen yli Kaikki, missä puskuroitu IO on hyvä — tiedosto-osoittimen takana tapahtuva peräkkäinen ennakkoluku — on suunnattu kuormaan, jota PDF:llä ei ole

Tämän artikkelin ensimmäinen versio väitti, että muistiin kuvattu tiedosto ratkaisee 32-bittisen muistiloppumisvirheen, johon TMemoryStream törmää 2 Gt:n syötteellä Tuo väite on väärä, ja tapa, jolla se on väärä, osoittaa oikean korjauksen suuntaan: liukuva kuvausikkuna Seuraavassa käydään läpi käyttökuvio, korjattu 32-bittinen tarina käännettävällä ikkunoidulla kuvaimella sekä järjestelmäkutsuaritmetiikka 1,8 Gt:n, 300 000 objektin testitiedostolla

Miksi PDF:n asettelu voittaa puskuroidut luvut

Kolme rakenteellista seikkaa muovaavat IO-kuviota Ensinnäkin navigointi perustuu siirtymiin: ristiviiteosataulukko kuvaa jokaisen objektinumeron absoluuttiseksi tavusijainniksi, eikä mikään vaadi niiden olevan järjestyksessä Vuosien inkrementaalisten päivitysten jälkeen objekti 4102 voi sijaita siirtymässä 1,6 Gt, kun taas objekti 4103 sijaitsee kohdassa 30 kt TFileStream-silmukka muuttaa jokaisen haun Seek-kutsuksi plus Read-kutsuksi, kahdeksi kernel-siirtymäksi, joissa puskuri ei tuo mitään hyötyä, koska seuraava haku on satoja megatavuja kauempana

Toiseksi objektivirrat (ISO 32000-1 §7.5.7) pakkaavat kymmeniä tai satoja pieniä sanakirjoja yhteen deflatoituun säiliöön Yhden 300 tavun sivusanakirjan hakeminen voi tarkoittaa 100 kt:n klusterin lukemista ja purkamista Kääntöpuoli: yhdessä kirjoitetut objektit luetaan yleensä yhdessä, joten klusterin kokoiseksi mitoitettu puskuri palvelee seuraavaa kymmenkuntaa hakua ilmaiseksi — formaatin hyödynnettävin säännönmukaisuus

Kolmanneksi linearisointi Linearisoitu tiedosto tuo ensimmäisen sivun ja vihjetaulukon eteen, jotta kuluttajat voivat lukea sen alusta loppuun Gigatavun kokoiset arkistot eivät juuri koskaan ole linearisoituja: linearisoinnin tuhoavat samat inkrementaaliset päivitykset ja yhdistämiset, jotka tekivät tiedostosta suuren Suunnittele pahin tapaus: pitkät hypyt, ei järjestystä, sisääntulo hännästä

PDF: hyppy hyppyltä -pääsymalli gigatavumittakaavan PDF-käsittelyssä, saapuen startxrefissä tiedoston hännässä ja sitten kulkiessa hajallaan olevia ristiviittaussiirtymiä
PDF-navigointi saapuu hännästä ja hyppää sitten sinne, minne ristiviitetaulukko osoittaa, mikä kaataa peräkkäisen ennakkoluvun

32-bittinen tarina, korjattuna

32-bittisellä Windows-prosessilla on 2 Gt käyttäjän osoiteavaruutta, ja MapViewOfFile-kutsu tavumäärällä nolla pyytää yhtä yhtenäistä varausta tiedoston kokoisena 2 Gt:n syötteellä tuo varaus ei voi onnistua: EXE:n, hajanaisten DLL:ien ja säievarojen jälkeen suurin vapaa yhtenäinen lohko tyypillisessä 32-bittisessä Delphi-prosessissa on jossain 700 Mt:n ja 1,4 Gt:n välillä Kutsu epäonnistuu virheellä ERROR_NOT_ENOUGH_MEMORY, samaan seinään, johon TMemoryStream.LoadFromFile törmää, vain siirrettynä varatusta RAM-muistista osoiteavaruusvaraukseen Koko tiedoston kuvaus ei ole korjaus 32-bittisellä alustalla, vain sama virhe paremmalta kuulostavien rajapintanimien takana

Korjaus on kahden kuvauksen tekemän asian erottaminen toisistaan CreateFileMapping luo osio-objektin eikä maksa lainkaan osoiteavaruutta, oli tiedostokoko mikä tahansa Vain MapViewOfFile kuluttaa osoiteavaruutta, eikä mikään pakota sitä kuvaamaan koko osiota: se ottaa vastaan 64-bittisen aloitussiirtymän ja näkymän pituuden Luo osio kerran, kuvaa 64–256 Mt:n näkymä jäsennettävän alueen yli, poista kuvaus ennen liukumista eteenpäin: osoiteavaruuden kustannus on yksi ikkuna, ei yksi tiedosto Yksi rajoitus: näkymän siirtymien on oltava SYSTEM_INFO.dwAllocationGranularity-arvon monikertoja, käytännössä 64 kt, joten pyyntö siirtymälle 1 000 000 pyöristetään alaspäin arvoon 983 040, ja kutsujan osoitinta siirretään eteenpäin erotuksen verran

Liukuikkunainen kuvain Delphissä

Alla oleva luokka kääri koko kurin: yksi osio-objekti, yksi elävä näkymä, rakeisuuden uudelleentasaus sekä ikkunarajan ylittävät luvut, jotka käsitellään kasvattamalla tuota yhtä näkymää sen sijaan, että kaksi ommeltaisiin yhteen

uses
  Winapi.Windows, System.SysUtils;

type
  TWindowedFileMapper = class
  private
    FFile: THandle;
    FMapping: THandle;
    FFileSize: Int64;
    FGranularity: DWORD;      // SYSTEM_INFO.dwAllocationGranularity
    FWindowSize: NativeUInt;  // oletusnäkymän koko
    FViewBase: PByte;         // nykyisen näkymän kanta (tasattu)
    FViewOffset: Int64;       // tiedostosiirtymä, jota FViewBase vastaa
    FViewSize: NativeUInt;    // nykyiseen näkymään kuvatut tavut
    procedure Unmap;
  public
    constructor Create(const FileName: string;
      WindowSize: NativeUInt = 64 * 1024 * 1024);
    destructor Destroy; override;
    function Map(Offset: Int64; Size: NativeUInt): PByte;
    procedure ReadBytes(Offset: Int64; var Buffer; Count: NativeUInt);
    property FileSize: Int64 read FFileSize;
  end;

constructor TWindowedFileMapper.Create(const FileName: string;
  WindowSize: NativeUInt);
var
  Info: TSystemInfo;
begin
  inherited Create;
  FFile := CreateFile(PChar(FileName), GENERIC_READ, FILE_SHARE_READ, nil,
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  if FFile = INVALID_HANDLE_VALUE then
    RaiseLastOSError;
  if not GetFileSizeEx(FFile, FFileSize) then
    RaiseLastOSError;
  // Osio-objekti ei varaa osoiteavaruutta, oli tiedostokoko mikä tahansa
  FMapping := CreateFileMapping(FFile, nil, PAGE_READONLY, 0, 0, nil);
  if FMapping = 0 then
    RaiseLastOSError;
  GetSystemInfo(Info);
  FGranularity := Info.dwAllocationGranularity;  // käytännössä 64 kt
  FWindowSize := WindowSize;
end;

destructor TWindowedFileMapper.Destroy;
begin
  Unmap;
  if FMapping <> 0 then CloseHandle(FMapping);
  if FFile <> INVALID_HANDLE_VALUE then CloseHandle(FFile);
  inherited;
end;

procedure TWindowedFileMapper.Unmap;
begin
  if FViewBase <> nil then
  begin
    UnmapViewOfFile(FViewBase);
    FViewBase := nil;
    FViewSize := 0;
  end;
end;

function TWindowedFileMapper.Map(Offset: Int64; Size: NativeUInt): PByte;
var
  AlignedOffset: Int64;
  Delta, MapSize: NativeUInt;
begin
  if (Offset < 0) or (Offset + Int64(Size) > FFileSize) then
    raise ERangeError.CreateFmt(
      'Map request at %d for %d bytes is outside the file',
      [Offset, Int64(Size)]);

  // Nopea polku: pyydetty alue on jo elävän näkymän sisällä
  if (FViewBase <> nil) and (Offset >= FViewOffset) and
     (Offset + Int64(Size) <= FViewOffset + Int64(FViewSize)) then
    Exit(FViewBase + NativeInt(Offset - FViewOffset));

  Unmap;  // liu'uta: älä koskaan pidä kahta näkymää samaan aikaan

  // Näkymien on alettava varausrakeisuuden mukaisesta rajasta
  AlignedOffset := Offset - (Offset mod FGranularity);
  Delta := NativeUInt(Offset - AlignedOffset);

  MapSize := FWindowSize;
  if MapSize < Size + Delta then   // pyyntö ylittää ikkunan lopun:
    MapSize := Size + Delta;       // kasvata tätä yhtä näkymää sen kattamiseksi
  if AlignedOffset + Int64(MapSize) > FFileSize then
    MapSize := NativeUInt(FFileSize - AlignedOffset);  // rajaa tiedoston loppuun

  FViewBase := MapViewOfFile(FMapping, FILE_MAP_READ,
    DWORD(AlignedOffset shr 32), DWORD(AlignedOffset and $FFFFFFFF),
    MapSize);
  if FViewBase = nil then
    RaiseLastOSError;

  FViewOffset := AlignedOffset;
  FViewSize := MapSize;
  Result := FViewBase + NativeInt(Delta);
end;

procedure TWindowedFileMapper.ReadBytes(Offset: Int64; var Buffer;
  Count: NativeUInt);
begin
  Move(Map(Offset, Count)^, Buffer, Count);
end;

Kaksi yksityiskohtaa kantaa painon Map-funktion alussa oleva nopea polku palauttaa osoittimen ilman kernel-siirtymää, kun pyydetty alue on jo elävän näkymän sisällä; objektivirtojen klusteroinnin ansiosta tämä on tavallinen tapaus, ja siitä säästöt tulevat Ja pyyntö, joka ylittää oletusikkunan lopun, kasvattaa MapSize-arvoa vain tuolle yhdelle näkymälle sen sijaan, että kaksi ommeltaisiin yhteen, mikä pitää ReadBytes-funktion yksirivisenä ja kutsujat vapaina osittaisten lukujen silmukoista

Ikkunan koko on antelias säädin: 64 Mt:lla 1,8 Gt:n tiedoston täysi pyyhkäisy on 29 näkymää, 256 Mt:lla se on 8, mutta jokainen varaus on vaikeampi sijoittaa pirstoutuneeseen 32-bittiseen tilaan, ja alle noin 16 Mt:n kohdalla hyppyraskaat tiedostot kuvaavat uudelleen tarpeeksi usein huomatakseen sen Missä tahansa 64–256 Mt:n välillä kuvausliikenne on tilastollista kohinaa

Järjestelmäkutsujen laskenta

Nyt aritmetiikka Testitiedosto: 1,8 Gt, 300 000 epäsuoraa objektia, keskimäärin noin 600 tavua hyötykuormaa kutakin kohti Objektikohtainen jäsennin hakee jokaisen SetFilePointerEx-kutsulla plus 4 kt:n ReadFile-kutsulla: 600 000 kernel-siirtymää Välimuistista tapahtuva lukujärjestelmäkutsu kestää edestakaisin noin 1,5 μs nykyisellä x64-laitteistolla, joten se on 600 000 × 1,5 μs ≈ 0,9 sekuntia puhdasta kernel-ylikuormaa ennen kuin yhtään tavua on jäsennetty — lämpimän välimuistin paras tapaus Kylmänä jokainen hyppy on laiteoperaatio: NVMe:n 4 kt:n satunnaislukujen noin 20 μs:n tehollisella viiveellä 300 000 niistä maksaa noin 6 sekuntia laiteaikaa; SATA-luokan tallennustilassa minuutteja

Lukemiset myös siirtävät väärää dataa: 300 000 × 4 kt työntää 1,2 Gt käyttäjäpuskureiden läpi toimittaakseen noin 180 Mt hyötykuormaa — kuusinkertainen vahvistus, joka tavu kopioidaan kernelistä käyttäjätilaan

Objektivirtaklustereiden kokoiseksi mitoitettu ennakkolukupuskuri on ensimmäinen rehellinen parannus: yksi 256 kt:n luku klusteria kohti yhden objektikohtaisen luvun sijaan leikkaa siirtymämäärän yhdellä tai kahdella suuruusluokalla Se on myös oikea työkalu silloin, kun kuvaus on hankalaa, yleensä verkko-osuuksilla

Ikkunoitu kuvain menee pidemmälle Täysi pyyhkäisy on 29 MapViewOfFile- ja 29 UnmapViewOfFile-kutsua, 58 eksplisiittistä siirtymää 600 000:ta vastaan Todellinen xref-ohjattu jäsennys ei ole puhdas pyyhkäisy, mutta nopea polku imee jokaisen haun elävän ikkunan sisään; metadatan indeksointiajo testiarkistolla asettui muutamaan sataan uudelleenkuvaukseen Kuvaus ei poista kernel-työtä: se muuntaa eksplisiittiset järjestelmäkutsut sivuvirheiksi, jotka muistinhallinta ratkaisee monisivuisina klustereina suoraan tiedostovälimuistista ilman käyttäjätilan kopiota, ja koskemattomat alueet eivät maksa mitään Päästä päähän indeksointiajo laski 23 s:sta kylmänä ja 7,1 s:sta lämpimänä objektikohtaisilla luvuilla 6,5 s:iin kylmänä ja 1,9 s:iin lämpimänä kuvaimen kanssa; jäljelle jää zlib-purku, ei IO

PDF: pirstoutunut 32-bittinen osoiteavaruus hylkää koko tiedoston MapViewOfFilen, kun taas CreateFileMapping-osio ja liukuväli 64 MB:n kartoitusikkuna onnistuvat
Yksi osioobjekti plus yksi elävä näkymä pitää 1,8 gigatavun PDF:n luettavana 32-bittisen Delphi-prosessin 2 gigatavun osoiteavaruudessa

Mihin FILE_FLAG_NO_BUFFERING sopii

FILE_FLAG_NO_BUFFERING ohittaa järjestelmävälimuistin vastineeksi tiukoista tasausvaatimuksista: siirtymät, pituudet ja puskuriosoitteet kaikki sektoritasattuja Se ansaitsee paikkansa yhden kierroksen peräkkäisissä töissä, jotka muuten täyttäisivät välimuistin tavuilla, joita kukaan ei lue kahdesti — eräajona tapahtuva koko arkiston uudelleensarjallistus tai linearisointiajo valmiin tulosteen yli 4–8 Mt:n tasatuilla puskureilla se lähestyy laitteen peräkkäistä kaistanleveyttä saastuttamatta välimuistia

Se on täysin väärä jäsentämiseen Satunnaiset xref-hypyt puskuroimattoman kahvan läpi muuttavat jokaisen 300 tavun sanakirjahaun täydeksi fyysiseksi luvuksi, eikä välimuistia ole imemään toista käyntiä — ja PDF-jäsennys käy alueilla toistuvasti uudelleen, koska eri sivut ratkeavat samoihin objektivirtoihin Puskuroimaton IO peräkkäiselle uudelleenkirjoitukselle, kuvattu tai välimuistitettu IO satunnaiselle jäsennykselle; lippu on kahvakohtainen, joten yksi putki voi pitää molempia samalla tiedostolla

64-bittisyys, työskentelyjoukot ja kirjoituspuoli

64-bittisessä käännöksessä osoiteavaruusvastaväite katoaa: anna tiedostokoko ikkunaksi, ja yllä oleva luokka rappeutuu yhdeksi täydeksi kuvaukseksi Ansa pitkään käynnissä olevissa palveluissa: vain luettavat tiedostoon sidotut sivut eivät veloita sitoumusta, joten sitoumuslaskurit pysyvät rauhallisina, mutta jokainen kosketettu sivu liittyy työskentelyjoukkoon; jäsennä suurin osa 1,8 Gt:sta, ja työskentelyjoukko kasvaa sitä vastaavasti, häätäen kaiken muun Rajatut ikkunat asettavat sille katon, joten liukuva kuvio pysyy oikeana oletuksena silloinkin, kun osoiteavaruutta on runsaasti

Kirjoituspuolella halvin IO on IO, jota ei koskaan tehdä PDF:n inkrementaalinen päivitysmekanismi (ISO 32000-1 §7.5.6) liittää muuttuneet objektit ja uuden ristiviiteosan alkuperäisten tavujen perään, jotka eivät koskaan liiku Yhden sivun leimaaminen 1,8 Gt:n arkistoon liittää kymmeniä kilotavuja; täysi uudelleenkirjoitus siirtää koko 1,8 Gt:n, viisi suuruusluokkaa erillään, ja liittäminen on puhdasta peräkkäistä tulostetta hännässä

Mihin losLab-kirjastot sopivat

Molemmat losLabin PDF-kirjastot toimittavat tämän kurin rajapintapinnalla HotPDF Direct File API lukee sivumäärät ja rakenteen tiedostokahvan kautta rakentamatta objektipuuta, kopioi ja purkaa salauksen tiedostotasolla ja kirjoittaa deltat BeginIncrementalUpdate-kutsun kautta — yllä oleva pelkkää liittämistä käyttävä strategia, paketoituna PDF Library for Delphi kulkee saman reitin Direct Access -kerroksellaan: suoratoistava lukija, joka kävelee ristiviiteosataulukon läpi paikallaan, hakee objektit laiskasti, poimii sivualueet tiedostosta tiedostoon ja tallentaa muokkaukset inkrementaalisina versioina Jos kirjoitat omaa jäsennintäsi, kuvainluokka on vapaasti käytettävissäsi; jos ajat asiakirjaputkea, anna kirjaston pitää ikkuna rehellisenä

Huom: Optimoitu IO-käsittely gigatavun kokoisille asiakirjoille on sisäänrakennettu suoraan Delphille ja C++Builderille tarkoitettuun HotPDF Delphi VCL Component -kirjastoon

PDF: pylväskaavio 600000 objekti kohtaisesta ReadFile-järjestelmäkutsusta vastoin klusterilukuennakkoa ja 58 ikunoitua kartoitinsiirtymää indeksoitaessa 1,8 GB:n PDF, joka sisältää 300000 objektia
Objektiittaiset luvut polttavat 600 000 järjestelmäkutsua testiarkistossa, kun taas ikkunoitu mappaaja vähentää täyden läpikäynnin 58 siirtymään