Tekninen artikkeli

Suurten Excel-työkirjojen suorituskyky Delphissä HotXLS-Delphi-komponentilla

Kun 300 000 rivin vienti ylittää muistibudjetin, syyksi syytetään yleensä rivimäärää. Rivimäärä on tavallisesti syytön. Suuren työkirjan kalliit osat syntyvät sivuvaikutuksina: tyylivarasto, johon kasvaa yksi merkintä solua kohden, koska muotoilu lisättiin silmukan sisällä, laskentataulukon XML, joka kootaan tallennushetkellä yhdeksi valtavaksi merkkijonoksi, tai miljoona samanlaista kaavarunkoa, jotka tallennetaan yksitellen. HotXLS, losLabin natiivi Delphi-kirjasto XLS- ja XLSX-tiedostoille, antaa sinulle täsmällisen säätimen jokaiseen näistä kustannuksista. Mikään niistä ei ole oletuksena käytössä, koska jokainen muuttaa kompromissia, joten varsinainen suorituskykytaito on tietää, mikä säädin vastaa mitäkin oiretta

Mihin suuri työkirja käyttää muistia

On kaksi erillistä muistijärjestelmää, joita on syytä tarkastella. Luonnin aikana muistissa oleva solumalli kasvaa jokaisesta koskettamastasi solusta: arvot, muotoilut ja kaavat muuttuvat kaikki olioiksi tai varastomerkinnöiksi. Tallennuksen aikana oletusarvoinen XLSX-polku lisäksi muodostaa jokaisen laskentataulukon XML:n laajaksi merkkijonoksi ennen sen pakkaamista zip-säiliöön, joten huippukulutus on malli plus suurimman taulukon sarjallistettu muoto. Työ, joka selviää rakennussilmukasta mutta kuolee SaveAs-kutsun sisällä, osuu toiseen järjestelmään eikä ensimmäiseen, eikä toisen korjaus tee mitään toiselle

Kaksi muistijärjestelmää Delphi HotXLS -suuressa työkirjatyössä: generaattorisilmukan rakentama muistinvarainen solumalli sekä suurimman taulukon sarjallistettu XML-merkkijono oletustallennuksen aikana, jonka StreamingWrite poistaa
Rakennussilmukka ja tallennuskutsu epäonnistuvat kahdessa eri muistitilassa, joten StreamingWrite litistää vain tallennushetken piikin, kun taas rakennuspolun muisti tarvitsee tyyli-poolin ja callback-vivut

Tiedostokoko noudattaa samankaltaista sääntöä: solut ovat vain yksi tekijä tyylien, jaettujen merkkijonojen, kaavojen, kuvien ja kommenttien rinnalla. Tarkastuskierros, jossa käytetään ForEachCell-kutsua ja taulukkokohtaisia kokoelmamääriä, kertoo, mikä resurssi todella hallitsee ongelmallista tiedostoa ennen kuin optimoit väärän kohteen. Mittauksessa on yksi hienovaraisuus: XLSX-puolen Sheet.Cells.Count ilmoittaa harvaan säilöön luotujen solujen määrän, ei käytetyn alueen pinta-alaa. Taulukko, jonka tiedot ovat 1000 kertaa 50 -suorakulmiossa ja jonka soluista puolet on tyhjiä, sisältää noin 25 000 solua, ei 50 000:ta. Ero on tärkeä, kun vertaat asiakkaan "valtavaa" tiedostoa testiaineistoihisi, sillä käytetyn alueen pinta-ala ja todellinen solupopulaatio voivat poiketa harvoissa talousasetteluissa kertaluokalla

StreamingWrite korjaa tallennuspolun, ei rakennuspolkua

Asettamalla TXLSXWorkbook.StreamingWrite := True vaihdat SaveAs-kutsun suoratoistavaan sarjallistajaan, joka kirjoittaa laskentataulukon XML:n suoraan zip-virtaan ja poistaa taulukkokohtaisen merkkijonovälivaiheen. Oletusarvo on yhteensopivuussyistä False, ja sen ottaminen käyttöön on yhden rivin muutos:

Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Bulk');
  for R := 1 to 100000 do
  begin
    Sheet.Cells[R, 1].Value := R;
    Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
    Sheet.Cells[R, 3].Value := R * 1.5;
  end;
  Book.StreamingWrite := True;   // arkin XML virtaa zip-muistiosaan
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Ole tarkka siitä, mitä tällä saat: silmukan rakentama solumalli vie täsmälleen yhtä paljon muistia kuin aiemmin. StreamingWrite tasoittaa tallennusajan piikin, mikä ratkaisee, valmistuuko eräajo vai epäonnistuuko se 95 prosentin kohdalla. Jos rakennussilmukka itse kuluttaa muistin loppuun, tarvitset seuraavat kaksi säädintä

Tyylivarastot: lisää kerran, käytä indeksiä uudelleen

HotXLS:n XLSX-muotoilu perustuu varastoihin: Book.Fonts.Add(...), Fills.AddSolid(...) ja Borders.Add(...) palauttavat 0-pohjaisen varastoindeksin, johon solut viittaavat. Identtisillä parametreilla silmukan sisällä kutsuttu Fonts.Add poistaa kaksoiskappaleet, joten se tuhlaa aikaa eikä tilaa. Alignments.Add käyttäytyy eri tavoin: se palauttaa jokaisella kutsulla uuden olion, joten solukohtainen kohdistuksen luonti kasvattaa varastoa lineaarisesti rivimäärän mukana. Yksi tapa kattaa molemmat tilanteet: ratkaise jokainen varastoindeksi kerran silmukan ulkopuolella ja määritä indeksit sen sisällä

HotXLS Delphi -tyylipoolin käyttö verrattuna: tuore Alignments.Add-objekti, luotuna kerran rivillä kohti, kasvattaa poolin lineaarisesti, kun taas silmukan yläpuolelle nostettu Fonts.Add-indeksi, ratkaistuna kerran, on jokaisen solun uudelleenkäyttämä ja nollasta alkava indeksi siirtyy yhdellä
Selvitä jokainen fontti-, täyttö-, reunus- ja tasausindeksi kerran silmukan ulkopuolella ja sijoita se 0-pohjainen pooli-indeksi yhdellä siirrettynä silmukan sisällä
// nosta pool-haut pois kuumasta silmukasta
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False);   // 0-pohjainen pool-indeksi
for C := 1 to 24 do
  Sheet.Cells[1, C].FontIndex := HeaderFont + 1;            // solut tallentavat 1-pohjaisena; 0 = oletus

+ 1 ei ole kirjoitusvirhe, ja sen unohtaminen on tässä se klassinen oireita synnyttävä virhe: varastot jakavat 0-pohjaisia indeksejä, kun taas solupuolen ominaisuudet käsittelevät arvoa 0 oletuksena, joten jokaista varastoindeksiä on siirrettävä yhdellä määrityksessä. Jos jätät siirron pois, otsikot hahmontuvat hiljaa työkirjan oletusfontilla. Kukaan ei huomaa virhettä ennen bränditarkastusta

Korvaa solukohtainen Variant-liikenne rivikutsupalautteilla

Jokainen Sheet.Cells[R, C].Value := X sisältää solun haku- tai luontitoiminnon sekä Variant-määrityksen. Muutamassa sadassatuhannessa solussa tämä solukohtainen kuormitus alkaa näkyä profiloinneissa. HotXLS tarjoaa kummassakin julkisivussa joukkokutsurajapinnat (ForEachCell ja ForEachRow lukemiseen, WriteCells ja WriteRows kirjoittamiseen), jotka siirtävät iteroinnin moottorin sisälle ja luovuttavat koodillesi kokonaisia rivejä kerrallaan:

procedure TLedgerExport.FillRow(Sender: TObject;
  SheetIndex, Row, FirstCol, LastCol: Integer;
  var Values: Variant; var Skip: Boolean; var Cancel: Boolean);
begin
  if Row > FCount then
  begin
    Cancel := True;     // lopeta koko kirjoitus
    Exit;
  end;
  Values := VarArrayOf([FRows[Row - 1].Account,
                        FRows[Row - 1].PostedOn,
                        FRows[Row - 1].Amount]);
end;

// yksi moottorikutsu satojentuhansien ominaisuusosumien sijaan
Sheet.WriteRows(1, 1, FCount, 3, FillRow);

Kutsupalautteen Skip-lippu jättää rivin koskematta keskeyttämättä toimintoa, ja Cancel lopettaa toiminnon varhain. Tämä on hyödyllistä, kun lähde on lukija, jonka pituus selviää vasta edetessä. Kun yhdistät rakennuksessa WriteRows-kutsun tallennuksen StreamingWrite-asetukseen, luontipolulle ei jää solukohtaista kuumaa kohtaa

XLS-julkisivun lukupuolen säätimet

Suurilla vanhoilla .xls-tiedostoilla on oma työkalupakkinsa. _DisableGraphics := True ennen Open-kutsua ohittaa piirrostason jäsentämisen kokonaan, mikä nopeuttaa sellaisten työkirjojen latausta, joihin on kertynyt vuosien varrella muotoja ja upotettuja kuvia. Rajoitus on ehdoton: piirrostaso puuttuu silloin mallista, joten tällaisen työkirjan tallentaminen kirjoittaa tiedoston ilman sen piirroksia. Varaa lippu vain luku -analyysitöille. SetTempDir ohjaa BIFF-kirjoittajan väliaikaistiedostot toiseen paikkaan, mikä on tärkeää palvelimilla, joiden oletusväliaikaissijainnilla on kiintiö tai hidas tallennusväline. UseSharedFormulas ryhmittelee toistuvat kaavarungot jaettujen kaavojen tietueiksi, mikä pienentää tiedostoja, joissa kaavasarake toistuu kuudenkymmenentuhannen rivin pituudelta

XLS-tietojen lukusilmukoissa on indeksointiansa, joka kannattaa nostaa esiin, koska puolustavasti käsiteltynä se kaksinkertaistaa työn ja huomaamatta jätettynä vioittaa tuloksia: UsedRange ilmoittaa rajansa FirstRow, LastRow, FirstCol ja LastCol 0-pohjaisina, kun taas Cells.Item[Row, Col] on 1-pohjainen. Käytetyn alueen läpikäyvän skannauksen on lisättävä yksi jokaiseen koordinaattiin solua käytettäessä, kuten muodossa Cells.Item[Row + 1, Col + 1], tai se lukee ruudukon yhden solun verran vinosti siirtyneenä, pudottaa hiljaa viimeisen rivin ja sarakkeen ja sisällyttää olemattoman ensimmäisen. ForEachCell-kutsupalautus kiertää ristiriidan kokonaan, mikä on yksi lisäsyy suosia sitä kokonaisten taulukoiden läpikäyntiin

Tunnustele tiedostot ennen niiden lataamista

Halvin suuren työkirjan toiminto on se, jonka jätät tekemättä. Molempien julkisivujen GetSheetNames luettelee tiedoston laskentataulukot lataamatta solutietoja. XLSX-toteutus lukee vain zip-tiedoston sisällä olevan työkirjaluettelon ja jättää työkirjaolion nimenomaisesti täyttämättä, kun taas XLS-julkisivu lopettaa skannauksen ensimmäiseen alivirtarajaan. Se on oikea ennakkotarkistus kysymykseen "mihin taulukkoon tämän tuontityön pitäisi kohdistua", ja CanReadEncrypted vastaa kysymykseen "onko tämä salattu säiliö" ennen toivotonta Open-yritystä

Esitarkistusvirta tuntemattomalle Excel-tiedostolle Delphissä HotXLS:llä: GetSheetNames luettelee laskentataulukot lataamatta soludataa, paluukoodi nollassa tai sen alapuolella tyhjentää luettelon ja merkitsee epäonnistumisen, CanReadEncrypted liputtaa salatut säiliöt ennen tuhoon tuomittua Openia, ja vasta sitten koko lataus ajaa
GetSheetNames ja CanReadEncrypted vastaavat siihen, mitä taulukkoa kohdistaa ja onko säiliö luettavissa, ennen kuin mitään soludataa jäsennetään
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
    raise Exception.Create('cannot enumerate sheets');   // virhe tyhjentää listan
  // valitse kohdearkki ja päätä sitten, kannattaako täysi avaaminen
finally
  Book.Free;
  Names.Free;
end;

Huomaa paluuarvokäytäntö: nämä tunnistelutoiminnot ilmaisevat epäonnistumisen nollaa pienemmillä tai yhtä suurilla arvoilla ja tyhjentävät tulosluettelon, joten testaa <= 0 sen sijaan, että vertaisit yhteen tiettyyn onnistumisarvoon

Sovita lähestymistapa työhön

Valvomattomiin liukuhihnoihin, jotka luovat useita suuria tiedostoja peräkkäin, kokonaisuuden täydentää vielä kaksi tapaa. Työkirjaoliot eivät ole säieturvallisia jaettaviksi, mutta mikään ei estä käyttämästä yhtä itsenäistä työkirjaa kutakin työsäiettä kohti, mikä rinnakkaistaa erämuunnoksen siististi. Kun tuloste menee levyn sijasta HTTP:hen, TStream-tallennuksen ylikuormitukset yhdistyvät StreamingWrite-asetukseen niin, ettei suuri vastaus koskaan konkretisoidu väliaikaistiedostoksi. Yksi käytännön alaviite pätee: virtaan tallennus kirjoittaa nykyisestä sijainnista kelaamatta taaksepäin, joten aseta Position := 0 ennen kuin luovutat virran vastauskehykselle. Suoratoistokirjoitusta ja eräajoja käsittelevä artikkeli kehittää palvelinpuolen mallin, ja tietokantavientiä käsittelevä artikkeli näyttää, mihin nämä säätimet sijoittuvat tietojoukko-ohjatussa raportissa

Pidä lopuksi yksi pahimman tapauksen testiaineisto jokaista raporttiperhettä varten ja mittaa sen ajoaika CI:ssä. Asiakirjojen luonnin suorituskykytaantumat ilmoittavat itsestään harvoin. Silmukan sisälle lisätty tyyli tai täydellä Open-kutsulla korvattu tunnustelu ei muuta toiminnallisuutta, ja yöajo kestää vain neljäkymmentä minuuttia pidempään. Ajastettu testi edustavalla puolen miljoonan solun aineistolla muuttaa ajautumisen punaiseksi koontiversioksi tuotantohäiriön sijaan

Arviointiversiot, joukkoluontiesimerkin sisältävät demoprojektit ja täydellinen API-viite ovat saatavilla HotXLS Delphi Component -sivulla