PDFlibPas, losLabin PDF-kirjasto Delphille ja C++Builderille, nopeuttaa renderöinti- ja sisällöntuotantopolkujaan korvaamalla neljä toistuvan työn mallia amortisoiduilla vastineilla: laiskan hakemiston sanakirja-avainhauille, esilasketun sRGB-gamma-hakutaulun, ensimmäisen tavun mukaan lohkoituksen sisältövirran operaattorijakelulle, ja TStringBuilderin toistuvan merkkijonoliitoksen sijaan. Yksikään näistä neljästä ei tullut yhdestä dramaattisesta löydöksestä — ne tulivat samasta ei-hohdokkaasta mallista profiilissa: pieni funktio, jota kutsutaan kerran per operaattori, kerran per pikseli tai kerran per merkki, jossa lineaarinen kustannus kutsun sisällä muuttuu neliölliseksi tai lähes neliölliseksi koko asiakirjan yli. Se on tämän artikkelin punainen lanka: neljä pientä, toisiinsa liittymätöntä korjausta, jotka hyökkäävät samaa ongelmamuotoa vastaan, plus jokaisen rehelliset rajat
Mihin sisältövirran renderöijä todella käyttää aikansa
PDFlibPas:n sisältövirran renderöijä ohjaa lähes kaiken tokenikohtaisen kustannuksensa neljän kapean pisteen kautta: resurssisanakirjahaut kentille /Resources, /ColorSpace, /Font ja /ExtGState; gammakorjaus jokaiselle puretulle pikselille Lab-, Indexed- tai ICC-tunnisteisessa kuvassa; operaattorinimen täsmäytys jokaiselle tokenille jokaisessa sisältövirrassa; ja merkkijonon rakentaminen missä tahansa, missä kirjasto rakentaa tulostetta — kirjaimellisen merkkijonon paettu tallennuksessa, XFDF-vienti, leima- ja muuttujatokenin laajennus. Jokainen näistä neljästä tekee pienen määrän työtä itsessään, ja jokainen niistä ajetaan tuhansia tai miljoonia kertoja realistisen asiakirjan yli, mikä on juuri se funktiomuoto, jossa O(n)- tai O(n²)-toteutusyksityiskohta lakkaa olemasta näkymätön ja alkaa olla profiilin ylin merkintä
Miksi resurssisanakirjahaut hidastuvat suuressa PDF:ssä?
TPDFDictionary.FindIndexByKeyName on se, mitä renderöijä kutsuu ratkaistakseen jokaisen /Resources-, /ColorSpace-, /Font- ja /ExtGState-haun, ja se käveli aiemmin Entries-taulukon edestä joka kutsulla — hyvä kolmen merkinnän Resources-sanakirjalle, kallis Form XObjectille tai ExtGState-painotteiselle sivulle, jossa samaa sanakirjaa tutkitaan jokaisella operaattorilla, joka koskettaa väriä tai graafista tilaa. PDFlibPas rakentaa nyt laiskan hakemiston heti, kun sanakirja ylittää DICT_HASH_THRESHOLD-arvon (16) merkinnän, ja jättää pienemmät sanakirjat lineaariseen skannaukseen, koska useimmat PDF-sanakirjat eivät koskaan kasva niin suuriksi, ja kolmen avaimen hakemisto maksaisi enemmän rakentaa kuin se säästää. Hakemisto on tasainen avoimen osoituksen taulu, avaimena PLAnsiStringHash, FNV-1a-tiiviste kanonisella siirtymäpohjalla 2166136261 ja alkuluvulla 16777619, valittu välttääkseen System.Generics.Collections-tuonnin jollekin näin kokoherkälle
Const
DICT_HASH_THRESHOLD = 16;
Function TPDFDictionary.LookupKeyIndex(Const Key: AnsiString): Integer;
Var
H, Probe: Integer;
Begin
Result:= -1;
If FKeyHashMask= 0 Then
Begin
// Not built yet; small dictionaries stay linear since the
// build cost would not amortize over a handful of entries.
If Length(Entries)> DICT_HASH_THRESHOLD Then
BuildKeyHash
Else
Exit;
End;
H:= PLAnsiStringHash(Key) And FKeyHashMask;
Probe:= 1;
While FKeyHash[H]<> -1 Do
Begin
If Entries[FKeyHash[H]].Key.Name= Key Then
Begin
Result:= FKeyHash[H];
Exit;
End;
H:= (H+ Probe) And FKeyHashMask;
Inc(Probe);
End;
End;
Hakemisto mitätöidään sen sijaan, että sitä ylläpidettäisiin inkrementaalisesti: jokainen muuttava kutsu — AddEntry, DeleteEntryByKeyName, Assign, AddDict — tyhjentää tiivisteen ja antaa seuraavan haun rakentaa sen uudelleen alusta. Tämä näyttää tuhlaukselta, kunnes huomaat, että sanakirja-avain on TPDFName-olio, ja TPDFName.SetTo voi nimetä uudelleen avaimen, joka jo istuu sanakirjan Entries-taulukossa, kulkematta minkään sanakirjan omista metodeista — inkrementaalisella hakemistolla ei ole tapaa havaita tuota uudelleennimeämistä, kun taas laiska yksinkertaisesti rakentuu uudelleen ja pysyy oikeana rakenteensa ansiosta. Tuon turvallisuuden hinta on O(n)-uudelleenrakennus ensimmäistä kertaa, kun suurta sanakirjaa kysytään kirjoituksen jälkeen, plus itse hakemistotaulukon muisti, karkeasti yksi Integer per paikka kahden kolmasosan täyttöasteella — pyöristysvirhe kourallliselle ylikokoisia sanakirjoja tyypillisessä asiakirjassa, ja todellinen kustannus, jonka PDFlibPas välttää maksamasta jokaisella pienellä, pitämällä kynnyksen siellä missä se on
sRGB-gamman esilaskeminen Power-kutsun sijaan per pikseli
TPDFSimpleColorManager.XYZ2RGB soveltaa sRGB-siirtofunktiota jokaiseen puretun Lab-, Indexed- tai ICC-pohjaisen kuvan pikseliin — 1.055 * Power(x, 1/2.4) - 0.055 lineaarisegmentin kynnyksen yläpuolella — ja Power(x, y):llä murtoluvulliselle y:lle ei ole halpaa suljettua muotoa Pascal-RTL:ssä: se hajoaa Ln(x):ksi ja sitten Exp(y * Ln(x)):ksi, ja tuo pari transkendenttisia kutsuja, ajettuna kolme kertaa per pikseli punaiselle, vihreälle ja siniselle kanavalle, on Lab- tai ICC-kuvan pikselikohtaisen purun hallitseva kustannus. PDFlibPas korvaa kolme pikselikohtaista Power-kutsua yhdellä haulla GSRGBGammaLUT-taulukkoon, 4096 merkinnän Double-taulukkoon, joka rakennetaan kerran EnsureSRGBGammaLUT-funktion kautta ja indeksoidaan pyöristämällä rajattu syöte lähimpään paikkaan
Const
SRGB_GAMMA_LUT_SIZE = 4096;
Var
GSRGBGammaLUT: Array [0..SRGB_GAMMA_LUT_SIZE- 1] Of Double;
GSRGBGammaLUTReady: Boolean= False;
Procedure EnsureSRGBGammaLUT;
Var
I: Integer;
X: Double;
Begin
If GSRGBGammaLUTReady Then
Exit;
For I:= 0 To SRGB_GAMMA_LUT_SIZE- 1 Do
Begin
X:= I/ SRGB_GAMMA_LUT_SIZE;
If X> 0.0031308 Then
GSRGBGammaLUT[I]:= 1.055* Power(X, 1/ 2.4)- 0.055
Else
GSRGBGammaLUT[I]:= 12.92* X;
End;
GSRGBGammaLUTReady:= True;
End;
Function SRGBGamma(X: Double): Double;
Var
Idx: Integer;
Begin
If X<= 0 Then
Result:= 0
Else If X>= 1 Then
Result:= 1
Else
Begin
Idx:= Round(X* SRGB_GAMMA_LUT_SIZE);
If Idx> SRGB_GAMMA_LUT_SIZE- 1 Then
Idx:= SRGB_GAMMA_LUT_SIZE- 1;
Result:= GSRGBGammaLUT[Idx];
End;
End;
4096 paikan taulukko [0, 1]-syötealueen yli antaa karkeasti kuusitoista kertaa 8-bittisen tulostekanavan resoluution, joten kvantisointi, jonka hakutaulu tuo, istuu lopullisen RGB-tavun esitettävissä olevan tarkkuuden alapuolella — taulukkohaku korvaa transkendenttisen matematiikan täällä ilman näkyvää tarkkuuskustannusta. Sama päättely näkyy sen vieressä funktiossa Lab2XYZ, jossa Power(LMN[i], 3) muuttui tavalliseksi LMN[i]*LMN[i]*LMN[i]:ksi: kokonaislukupotenssi ei tarvitse Ln/Exp-pareja ylipäätään, joten se ei ole lainkaan hakutaulukompromissi, vain redundantti Power-kutsu poistettu. Hakutaulutemppu kannattaa vain, koska siirtofunktio on puhdas funktio yhdestä Double-arvosta — se ei laajenisi puhtaasti värimuunnokseen, joka riippuisi useista pikseliarvoista tai enemmästä tilasta kuin tuo
Miten jaat 73 sisältövirran operaattoria nopeasti?
ContentOperatorFromName kutsutaan kerran jokaiselle tokenille, jonka PDFlibPas lukee sisältövirrasta, täsmäyttäen sitä ISO 32000-1 taulukon 51 täyttä 73 operaattorin joukkoa vasten — w:stä ja q:sta harvoin nähtyihin d0- ja d1-Type 3-glyfimittaoperaattoreihin asti — ja se käveli aiemmin tuon listan lineaarisesti jokaisella yksittäisellä tokenilla, joten sivu, jolla oli muutama tuhat operaattoria, tarkoitti muutamaa tuhatta lineaarista skannausta saman 73 merkinnän taulukon yli. PDFlibPas lohkoo nyt taulukon operaattorin ensimmäisen tavun mukaan käynnistyksessä, kiinteään AnsiChar-indeksoituun paikkataulukkoon, joten haku muuttuu yhdeksi taulukkoindeksiksi plus skannaukseksi vain kourallisen operaattoreita, jotka jakavat tuon ensimmäisen merkin
Type
TOpSlot= Record
Count: Integer;
Ops: Array [0..15] Of TPDFContentOperator;
End;
Var
GOpBuckets: Array [AnsiChar] Of TOpSlot;
GBucketsReady: Boolean= False;
Function ContentOperatorFromName(Const Name: AnsiString): TPDFContentOperator;
Var
Ch: AnsiChar;
Slot: ^TOpSlot;
I: Integer;
Op: TPDFContentOperator;
Begin
Result:= coUnknown;
If (Name= '') Then
Exit;
EnsureOpBuckets;
Ch:= Name[1];
Slot:= @GOpBuckets[Ch];
If Slot^.Count= 0 Then
Exit;
For I:= 0 To Slot^.Count- 1 Do
Begin
Op:= Slot^.Ops[I];
If (PDFContentOpInfo[Op].Name= Name) Then
Begin
Result:= Op;
Exit;
End;
End;
End;
PDF-operaattorit ovat kirjainkoosta riippuvaisia — w ja W, f ja F, sc ja SC ovat kaikki eri operaattoreita — joten GOpBuckets avaimena on raaka tavu, ja jäljelle jäävä vertailu lohkon sisällä on tavallinen, kirjainkokoherkkä AnsiString-yhtäläisyys. Taulukko on mitoitettu 16 paikkaan per kirjain, mikä kattaa mukavasti tämän päivän taulukon — kiireisin lohko, T, kantaa kolmetoista operaattoria, koska lähes jokainen tekstitila- ja tekstinsijoitusoperaattori alkaa sillä — mutta EnsureOpBuckets lakkaa hiljaa lisäämästä lohkoon heti, kun sen määrä saavuttaa 16:n, joten lohko, joka koskaan tarvitsisi neljännentoista merkinnän, epäonnistuisi hiljaa eikä äänekkäästi: operaattori ratkeaisi coUnknown:ksi ilman poikkeusta osoittamassa miksi. Se on ylläpitokustannus siitä, että vaihdat sulavasti heikkenevän tietorakenteen sellaiseen, joka ei heikkene — se jakelee nopeammin, koska se ei koskaan tarvitse rajatarkistettua kasvua, ja se tarvitsee ihmisen tarkkailemassa sitä yhtä lohkoa, joka on lähellä kattoaan
O(n²):n leikkaaminen pois merkkijonon rakentamisesta
Pascalin Result := Result + Fragment -malli varaa uudelleen ja kopioi koko kertyneen merkkijonon jokaisella iteraatiolla, joten N-merkkisen tulosteen rakentaminen yksi fragmentti kerrallaan maksaa O(n²) O(n):n sijaan — helppo ohittaa katselmuksessa, koska jokainen rivi näyttää yhdeltä halvalta lisäykseltä, ja kallis käytännössä, koska PLDirectEscapeLiteralString ajetaan jokaisella tallennuksen aikana kirjoitetulla kirjaimellisella PDF-merkkijonolla ja XFDFXMLEscape ajetaan jokaisella XFDF:ään viedyllä kenttäarvolla. PDFlibPas korjaa nämä kaksi eri tekniikoilla, valittuina sen mukaan, mitä kukin funktio voi ennustaa etukäteen. PLDirectEscapeLiteralString tietää tulostepituutensa ennen kuin se kirjoittaa yhtäkään tavua — yksi kierros luokittelee jokaisen merkin tavalliseksi tai paetuksi ja summaa kokonaismäärän, SetLength varaa kerran, ja toinen kierros täyttää puskurin indeksin mukaan. XFDFXMLEscape ei voi halvalla ennustaa tulostepituuttaan, koska Unicode-kenttäteksti vaihtelee liikaa etukäteen laskettavaksi, joten se liittää sen sijaan TStringBuilder-olioon, esimitoitettuna karkeasti syötteen pituuteen
Function XFDFXMLEscape(Const W: WideString): WideString;
Var
I: Integer;
Builder: TStringBuilder;
Begin
// TStringBuilder avoids the O(n^2) WideString concatenation that
// XFDF export used to hit on every field value
Builder:= TStringBuilder.Create(Length(W)+ 16);
Try
For I:= 1 To Length(W) Do
Begin
Case W[I] Of
'&': Builder.Append('&');
'<': Builder.Append('<');
'>': Builder.Append('>');
// ...'"', tab, CR and LF cases follow the same shape
Else
Builder.Append(W[I]);
End;
End;
Result:= Builder.ToString;
Finally
Builder.Free;
End;
End;
Valinta näiden kahden välillä koskee todella sitä, mitä tiedät ennen silmukan alkamista. Laske-sitten-täytä on nopeampi näistä kahdesta, kun tulostekoko on halpa laskea, koska se ei tee lainkaan uudelleenvarauksia eikä kirjanpitoa Integer-laskurin lisäksi, mutta se tarkoittaa luokittelulogiikan kirjoittamista kahdesti — kerran laskemista varten, kerran tuottamista varten — mikä on oma ylläpitoriskinsä, jos nuo kaksi kopiota ajautuvat erilleen. TStringBuilder luopuu pienestä osasta tuota huippulävistyskykyä kirjoittamalla logiikan kerran ja saamalla amortisoidut O(1)-lisäykset geometrisesta puskurin kasvusta, mikä on turvallisempi oletus aina, kun tulostekokoa ei ole helppo tietää etukäteen
Mihin tämä malli soveltuu, ja mihin ei
Kaikki neljä yllä olevaa korjausta ovat esimerkkejä yhdestä ideasta: löydä kutsu, joka ajetaan kerran per syötteen yksikkö — per sanakirja-avain, per pikseli, per operaattoritoken, per merkki — ja korvaa sen lineaarinen tai ennustamaton kustannus esilasketulla taulukolla, hakemistolla tai esimitoitetulla puskurilla. Mikään tästä ei ole PDF-spesifistä; Delphi-palvelu, joka ratkaisee saman hakuavaimen tuhansia kertoja per pyyntö, muuntaa arvoja tiukassa silmukassa, jakelee kiinteän tokenisanaston perusteella tai rakentaa pitkiä merkkijonoja merkki kerrallaan, osuu samoihin vikamalleihin ja ottaa samat korjaukset. Mihinkään näistä neljästä muutoksesta ei kosketa samanaikaisuutta tai muistijalanjälkeä: nopeampi yksisäikeinen sanakirjahaku ei tee mitään kahdelle säikeelle, jotka kilpailevat samasta TPDFlib-instanssista, mikä on rakenteellinen ongelma, joka käsitellään erikseen artikkelissa säikeidenturvallisuus rinnakkaisessa sivunrenderöinnissä, eikä se tee mitään PDF:lle, joka on liian suuri ladattavaksi muistiin olio-puuna ylipäätään, mikä on se, mitä varten Direct Access -kerros PDFlibPas:ssa on, käsitelty artikkelissa gigatavuisten PDF-tiedostojen yhdistäminen ja jakaminen
Tässä käsitelty sanakirja-, värienhallinta-, sisältövirran jakelu- ja merkkijononrakennuskoodi toimitetaan osana vakiomuotoista PDFlibPas:aa, losLabin PDF-kirjastoa Delphille ja C++Builderille, ilman että mikään näistä vaatii lisämääritystä