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ä
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
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