Tehnički članak

HotXLS graf ovisnosti: indeksiranje izlaza array formula

HotXLS 2.383.1, izvorna Excel biblioteka za Delphi i C++Builder, gradi bridove ovisnosti formula kroz indeks izlaznih intervala: čvorovi formula ostaju sortirani po sidrenoj ćeliji, a segment stablo koje drži najveći izlazni redak (OutRow2) svakog podstabla pušta TXLSDepGraph.BuildEdges da preskoči cijele blokove formula koje ne mogu dosegnuti referencirani raspon. Na Win32 radnoj knjigi s otprilike 100.000 formula forsirano preračunavanje palo je sa 18.488 sekundi na 102–109 milisekundi

Nitko ne profilira graf ovisnosti dok batch posao koji je uzimao sekundu ne počne uzimati dvadeset. Graf se gradi iznova kad god se promijeni topologija formula — prvi Recalculate nakon učitavanja ili generiranja radne knjige, ili bilo koji prolaz nakon što je graf poništen — i u pre-fix traceu taj je prvi prolaz sam uzeo 16.074 ms. Evaluacija nikad nije bila problem; odlučivanje tko o kome ovisi jeste

Zašto je preračunavanje 100.000 formula trajalo 18 sekundi?

Stari graditelj bridova bio je kvadratan u broju formula na listu. Za svaki raspon ovisnosti BuildEdges je binarno pretražio prozor kandidatskih čvorova i zatim svaki testirao s RangeIntersectsOutput, a taj je prozor počinjao na samom vrhu referenciranog lista. Ključevi čvorova dolaze iz XLSDepMakeKey, koji pakira indeks lista od bita 34 naviše, redak u bitove 14–33 i stupac u bitove 0–13, pa je donja granica (Sheet1, 0, 0) značila "svaku formulu od retka 1 do dna referenciranog raspona"

// Prije 2.383.1 - TXLSDepGraph.BuildEdges, za raspon ovisnosti r čvora d
LowerKey := XLSDepMakeKey(FRanges[r].Sheet1, 0, 0);   // vrh lista
UpperKey := XLSDepMakeKey(FRanges[r].Sheet2, FRanges[r].Row2, 16383);
// ...dvije binarne pretrage nad FNodeOrder daju prozor [i, Lo)...
while i < Lo do
begin
  NodeIndex := FNodeOrder[i];
  if RangeIntersectsOutput(FRanges[r], FNodes[NodeIndex]) then
  begin
    // tvrdi brid ili LookupScan brid, dedupliciran kroz EdgeStamp / ScanStamp
  end;
  Inc(i);
end;

Testni primjer koji je ovo izvukao je običan kaskadni model: A2:A50000 svaki dodaje jedan ćeliji iznad, a B1:B50000 svaki udvostručuje susjeda u stupcu A. Referenca na redak r zato je provlačila oko 2r kandidata kroz test pravokutnika, pa je jedna gradnja grafa napravila reda pet milijardi provjera presjeka — procjena "na papiru", ali slaže se s 18,5 sekundi na satu. Svaka je provjera rekla "ne" osim jedne ili dvije

Što je preračunavanje 100.000 formula u HotXLS-u učinilo trajnim 18 sekundi: stari BuildEdges binarno je pretražio prozor počevši od ključa (Sheet1, 0, 0), vrha referenciranog lista, i svakog kandidata testirao s RangeIntersectsOutput, pa je kaskadni primjer provlačio oko 2r kandidata po referenci kroz otprilike pet milijardi provjera presjeka
Ključ čvora pakira list, redak i stupac u jednu vrijednost, pa je donja granica (Sheet1, 0, 0) značila da u test pravokutnika ulazi svaka formula od retka 1 do dna referenciranog raspona

Zašto graditelj bridova ne može početi pretragu na referenciranom retku?

Zato što formula polja sidrena iznad raspona može posjedovati ćelije unutar njega. Svaki TXLSDepNode opisuje izlazni pravokutnik od svog sidra (Row, Col) do (OutRow2, OutCol2), a CSE formula polja dobiva jedan čvor za cijeli svoj pravokutnik, kako to objašnjava članak o inkrementalnom preračunavanju i grafu ovisnosti. Korijen sidren na A1 koji puni A1:A10 mora i dalje primiti brid od formule koja čita samo A5; počnete li binarnu pretragu na retku 5, taj brid tiho nestaje, što znači zastarjelu keširanu vrijednost u isporučenom izvještaju umjesto sporog. Upit je zapravo dvosmjerni — sidro na ili prije Row2, izlaz koji doseže barem Row1 — i jedan sortirni redoslijed ne može odgovoriti na obje polovice. Višećelijski rezultati javljaju se i u modernim radnim knjigama, a članak o dynamic array spill formulama pokriva kako se proliveni rasponi ponašaju u HotXLS-u

Zašto HotXLS graditelj bridova ne može početi pretragu na referenciranom retku: CSE polje sidreno na A1 koje puni A1:A8 posjeduje jedan čvor ovisnosti, pa formula u D5 koja čita samo A5 mora i dalje dosegnuti sidro na retku 1, a naivna pretraga od retka 5 izgubila bi brid i isporučila zastarjelu keširanu vrijednost
Upit je zapravo dvosmjerni, sidro na ili prije Row2 i izlaz koji doseže barem Row1, i jedan sortirni redoslijed ne može obuhvatiti obje polovice odjednom

Segment stablo maksimalnih izlaznih redaka

HotXLS zadržava sidreni sort za gornju granicu i dodaje augmentirano segment stablo za donju granicu. BuildNodeIndex sortira FNodeOrder po ključu čvora kao i prije, a zatim BuildMaxOutRowTree puni FNodeMaxOutRow2 (alociran na četiri unosa po čvoru) najvećim OutRow2 nađenim pod svakim podstablom. QueryNodeTree silazi samo unutar ključnog prozora i napušta svako podstablo čiji maksimalni izlazni redak leži iznad FRanges[r].Row1, jer nijedna formula u njemu ne može dosegnuti referencirane retke. Listovi koji prežive i dalje prolaze potpuni test RangeIntersectsOutput, pa se rasponi listova i stupci provjeravaju točno kao i prije

// TXLSDepGraph.BuildNodeIndex / BuildEdges od 2.383.1 (blago sažeto)
procedure BuildMaxOutRowTree(ATreeIndex, ALeft, ARight: Integer);
var
  Mid: Integer;
begin
  if ALeft = ARight then
  begin
    FNodeMaxOutRow2[ATreeIndex] := FNodes[FNodeOrder[ALeft]].OutRow2;
    Exit;
  end;
  Mid := (ALeft + ARight) shr 1;
  BuildMaxOutRowTree(ATreeIndex * 2, ALeft, Mid);
  BuildMaxOutRowTree(ATreeIndex * 2 + 1, Mid + 1, ARight);
  FNodeMaxOutRow2[ATreeIndex] := Max(FNodeMaxOutRow2[ATreeIndex * 2],
    FNodeMaxOutRow2[ATreeIndex * 2 + 1]);
end;

procedure QueryNodeTree(ATreeIndex, ALeft, ARight, ALower, AUpper: Integer);
var
  Split: Integer;
begin
  // izvan ključnog prozora, ili nijedan izlaz u ovom podstablu ne doseže Row1
  if (ARight < ALower) or (ALeft >= AUpper) or
     (FNodeMaxOutRow2[ATreeIndex] < FRanges[r].Row1) then
    Exit;
  if ALeft = ARight then
  begin
    Inc(FEdgeCandidateChecks);
    if RangeIntersectsOutput(FRanges[r], FNodes[FNodeOrder[ALeft]]) then
    begin
      // nepromijenjeno: EdgeStamp / ScanStamp suzbijanje, AddDependent / AddScanDependent
    end;
    Exit;
  end;
  Split := (ALeft + ARight) shr 1;
  QueryNodeTree(ATreeIndex * 2, ALeft, Split, ALower, AUpper);           // prvo lijevo podstablo
  QueryNodeTree(ATreeIndex * 2 + 1, Split + 1, ARight, ALower, AUpper);  // čuva stari redoslijed
end;

Rekurzija lijevo-prije-desno nije stilski izbor. Preživjeli listovi posjećuju se točno u redoslijedu kojim ih je posjećivala stara while petlja, pa se polja Dependents i Precedents pune u istom slijedu i topološki redoslijed ostaje determinističan. Isto vrijedi za dvije vrste bridova: tvrdi brid zabilježen prvi i dalje suzbija kasniji LookupScan brid za isti par, dok scan brid zabilježen prije tvrdog zadržava svoje mjesto — razlika koja lookup rasponima sprječava proizvodnju lažnih kružnih referenci. Po referenci trošak pada s veličine prozora na O((k + 1) log n), gdje je k broj formula čiji izlaz stvarno doseže referencirane retke

Kako HotXLS 2.383.1 indeksira izlaze formula polja: čvorovi ostaju sortirani po sidrenom ključu, BuildMaxOutRowTree čuva najveći OutRow2 svakog podstabla u FNodeMaxOutRow2, a QueryNodeTree napušta svako podstablo koje ne može dosegnuti Row1, pa samo preživjeli listovi prolaze RangeIntersectsOutput istim lijevo-prije-desno redoslijedom kao prije
Orezivanje spušta trošak po referenci s veličine prozora na O((k + 1) log n), dok identičan redoslijed posjeta zadržava polja Dependents i Precedents i topološki redoslijed determinističnima

Što indeks izlaza garantira, i kako se provjerava?

TXLSDepGraph proizvodi iste bridove u istom redoslijedu kao i prije, a novo svojstvo EdgeCandidateChecks broji koliko je izlaznih pravokutnika zadnja gradnja stvarno testirala, pa je tvrdnja mjerljiva a ne retorička. Regresijski test EdgeBuildDeepChainsCheckOneCandidatePerDependency gradi lance točkastih referenci od 1.024 i 100.000 čvorova, ubačenih obrnutim redoslijedom da prisili prostorni sort, i tvrdi točno N − 1 provjeru — 99.999 za dugi lanac — plus očekivani precedent, dependent i topološki redoslijed za svaki čvor. Prateći testovi pokrivaju korijene polja ubačena izvan reda preko raspona listova, duplicirane tvrde i lookup-scan reference (10 provjera, sa suzbijajućim pravilima gore), i ponovnu gradnju nakon AddNode, koja briše sortirnu zastavicu pa sljedeći BuildEdges ili NodeIndexOf gradi stablo iznova i resetira brojač umjesto da ga akumulira

Izmjereno: sa 18,5 sekundi na oko 0,1 sekundu

Pre-fix Win32 trace, zadržan u performansnoj bazi projekta za verziju 2.383.0, zabilježio je dva forsirana preračunavanja od 18.488 ms i 19.578 ms. Nakon indeksiranja, tri serijalna fokusirana pokretanja po arhitekturi mjerila su 102,332–109,429 ms na Win32 i 116,990–133,995 ms na Win64, otprilike 170 do 180 puta brže na Win32; pre-fix Win64 baza nije zabilježena, pa se Win64 ubrzanje ne tvrdi. Isti su pokreti prošli postojeću kapiju koja audit preračunavanja samo za čitanje drži unutar 1,35 puta forsiranog preračunavanja. Apsolutni brojevi ovise o stroju i njegovom opterećenju, pa radno opterećenje reproducirajte na vlastitom hardveru prije citiranja

uses
  System.SysUtils, System.Diagnostics, lxHandle;

procedure TimeChainRecalc;
var
  Wb: TXLSWorkbook;
  Sh: TXLSWorksheet;
  I, Failed: Integer;
  Watch: TStopwatch;
begin
  Wb := TXLSWorkbook.Create;
  try
    Sh := Wb.Sheets.Add;
    Sh.Cells[1, 1].Value := 1;
    for I := 2 to 50000 do                     // lanac od 49.999 karika u stupcu A
      Sh.Cells[I, 1].Formula := '=A' + IntToStr(I - 1) + '+1';
    for I := 1 to 50000 do                     // 50.000 dependenata u stupcu B
      Sh.Cells[I, 2].Formula := '=A' + IntToStr(I) + '*2';

    Watch := TStopwatch.StartNew;
    Failed := Wb.Recalculate;                  // prvi poziv gradi graf
    Watch.Stop;
    Writeln(Format('%d formulas not evaluated, %.1f ms',
      [Failed, Watch.Elapsed.TotalMilliseconds]));
  finally
    Wb.Free;
  end;
end;

Gdje indeks izlaza prestaje pomoći?

Stablo orezuje samo po recima, i to ostavlja nekoliko poštenih granica vrijednih pozornosti prije nego model velikih razmjera dizajnirate oko njega

  • Promašaji po stupcu i dalje se plaćaju na listovima: 2.626 formula koje pune A100:Z200 sve dosežu redak 100, pa referenca na AA100:AA200 svaku testira prije odbijanja
  • Široke reference poput raspona cijelog stupca stvarno imaju mnogo precedenata; indeks uklanja uzaludne provjere, ne stvarne bridove, i gradnja tih bridova i dalje je proporcionalna njihovom broju
  • Za reference koje prelaze preko više listova, pohranjeni maksimum ignorira list, pa formule na međulistovima s dubokim izlazima dosežu test listova; rezultati ostaju točni, samo je orezivanje slabije
  • Stablo košta četiri cijela broja po čvoru formule, oko 1,6 MB za 100.000 čvorova, i bilo koji AddNode ga poništava, pa promjene topologije plaćaju potpuno O(n log n) preslagivanje plus O(n) gradnju stabla na sljedećoj gradnji bridova

Isti kvadratni oblik u kloniranju imena report bandova

Verzija 2.383.2 popravila je srodan problem u TXLSXDefinedNames.UniqueCloneName: svako kopirano definirano ime ponovno je pokretalo sufiksnu pretragu od _2, pa su ponovljene kopije report bandova rasle kvadratno u pretragama imena. Indeks scoped imena sada čuva sufiksni trag po baznom imenu i po opsegu te ponovno provjerava zadnji vraćeni kandidat, jer pozivatelj ga možda stvarno ne doda; brisanje, preimenovanje ili promjena opsega imena poništava indeks, što vraća imenovanje prvog slobodnog. U regresijskoj jedinici 1.024 uzastopna klona treba 5.088 kandidatskih pretraga, a četiri izmjenična bazna imena 5.039, dok su minimumi report benchmarka pali s otprilike 240 ms na 18–20 ms. Timing kapija report bandova i dalje nije stabilna — tri od šest pokretanja prešla su njen omjer 1,05 u prvom pokušaju nakon popravka — i performansna povijest te propuste drži zabilježene umjesto da štima prag dok ne prođe

Ako Vaša Delphi ili C++Builder aplikacija generira ili preračunava velike Excel radne knjige, HotXLS Excel komponenta za Delphi i C++Builder isporučuje ovaj indeksirani graf ovisnosti u preračunskom engineu obje svoje klase radnih knjiga, classic i XLSX