Technischer Artikel

HotXLS-Lookup-Scans und falsche Zirkelbezüge

Setzen Sie =VLOOKUP(A1,B:B,1) in eine Zelle der Spalte B, und Excel berechnet sie ohne Murren. Geben Sie dieselbe Arbeitsmappe an eine Recalculation-Engine mit Abhängigkeitsgraph, erhalten Sie wahrscheinlich einen Zirkelbezug-Fehler, denn die Formel hängt von einer Range ab, die die Formel enthält. HotXLS meldete genau das bis v2.361.98. Der Fix ist kein Sonderfall für Ganzspalten-Ranges; er ist eine Unterscheidung zwischen zwei Arten von Abhängigkeitskante, die eine Tabellen-Engine braucht und ein schlichter gerichteter Graph nicht hat

Das Lookup-Array-Argument der Lookup-Familie – LOOKUP, MATCH, HLOOKUP, VLOOKUP, XLOOKUP und XMATCH – ist nun als Scan-Referenz markiert. Eine Scan-Referenz säht weiterhin Unsauberkeit, also berechnet eine Bearbeitung einer Zelle innerhalb der Range die Formel neu, aber sie trägt nie zur Zykluserkennung oder zur Auswertungsordnung bei. Echte Zyklen werden weiterhin gefunden; die falschen sind weg

Warum erlaubt Excel eine Lookup-Range, die die Formel enthält?

Weil dieses Argument nicht so konsumiert wird wie ein arithmetischer Operand. Die Lookup-Familie durchsucht die Range nach gecachten Werten und gibt einen Treffer zurück; sie verlangt nicht, dass die Range zuvor vollständig ausgewertet wurde. Excel behandelt eine sich selbst überlappende Lookup-Range so, als lese sie das, was diese Zellen gerade halten, was dieselbe Semantik ist, die es auf jede nicht-iterative Arbeitsmappe anwendet: Zellen, die in diesem Durchlauf noch nicht neu berechnet wurden, steuern ihren zuletzt berechneten Wert bei

Ganzspalten-Referenzen machen das zum gewöhnlichen Fall statt zum exotischen. B:B ist die idiomatische Art, „die gesamte Lookup-Tabelle“ zu schreiben, in einem Blatt, in dem Zeilen angehängt werden, und jede Formel, die in Spalte B lebt, sitzt damit in ihrer eigenen Lookup-Range. Finanzmodelle, Abstimmungsblätter und Audit-Arbeitsmappen tun das ständig, meist ohne dass jemand bemerkt, dass die Range sich überlappt

Zelle B7 hält VLOOKUP(A1,B:B,1) in ihrer eigenen Ganzspalten-Lookup-Range B:B, eine Selbstüberlappung, die Excel aus gecachten Werten ohne Murren berechnet
Ganzspalten-Lookup-Ranges machen Selbstüberlappung zum Normalfall in Finanzmodellen und Audit-Arbeitsmappen, nicht zur exotischen Ecke

Was ein Abhängigkeitsgraph mit derselben Formel macht

HotXLS berechnet inkrementell neu, was einen echten Abhängigkeitsgraph verlangt: Knoten für Zellen, Kanten für Referenzen, eine topologische Ordnung für die Auswertung und einen Durchlauf über stark zusammenhängende Komponenten, um Zyklen zu klassifizieren. Diese Maschinerie beschreibt der Artikel zur inkrementellen Neuberechnung, und genau deshalb erschien der Falsch-Positiv

Zieht man aus =VLOOKUP(A1,B:B,1) in Zelle B7 die Abhängigkeiten, liefert das zweite Argument eine Range, die B7 selbst enthält. Der Graph hat nun eine Selbstschleife. Der Eingangsgrad dieses Knotens erreicht nie null, also kann der topologische Durchlauf ihn nie einplanen, und der Komponentendurchlauf klassifiziert ihn als Zyklus. Die Engine schließt korrekt über den Graph, den sie bekam. Der Graph ist das falsche Modell, denn er codiert einen Kantentyp, wo die Tabelle zwei hat

Die B:B-Lookup-Range gibt dem Graphknoten B7 eine Selbstschleife, der Eingangsgrad erreicht nie null, und HotXLS vor v2.361.98 meldete einen falschen Zirkelbezug
Die Recalculation-Engine schloss korrekt über den Graph, den sie bekam; der Graph war das falsche Modell für eine Tabelle

Zwei Kantenklassen, ein Graph

Die Änderung ergänzt ein Flag am aufgelösten Referenzdatensatz, TXLSDepRange.LookupScan, das der Abhängigkeitsextraktor setzt, wenn er das Lookup-Array-Argument einer der sechs Funktionen durchläuft. Downstream werden Kanten aus diesen Referenzen getrennt von gewöhnlichen Kanten gespeichert: Der Graphknoten führt ScanDependents- und ScanPrecedents-Listen neben seinen normalen Abhängigen- und Vorgänger-Listen

Die Trennung macht die Semantik richtig. Scan-Kanten werden von der Dirty-Propagation durchlaufen, also markiert eine Bearbeitung irgendwo in B:B B7 weiterhin als dirty, und B7 berechnet neu. Scan-Kanten zählen nie in den Eingangsgrad und gelangen nie in den Komponenten-Builder, also können sie keinen topologischen Deadlock erzeugen und nicht als Zyklus klassifiziert werden. Beide Graph-Implementierungen in der Bibliothek, der klassische Graph je Arbeitsmappe und der arbeitsmappenübergreifende Workspace-Graph, der die Komponentenanalyse trägt, wurden gemeinsam geändert; sie auseinanderdriften zu lassen, würde eine Arbeitsmappe produzieren, die unterschiedlich neu berechnet, je nachdem, ob sie allein oder als Teil eines Workspace geöffnet wurde

Scan-Kanten aus TXLSDepRange.LookupScan treiben die Dirty-Propagation in ScanPrecedents und ScanDependents, zählen aber nie in Eingangsgrad oder Zyklen
Bearbeitungen in B:B markieren die Formel weiterhin als dirty, doch Scan-Kanten können den topologischen Durchlauf nicht blockieren oder einen Zyklus erzeugen
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Ledger');
    Sheet.Cells[1, 1].Value := 'ACC-4471';
    Sheet.Cells[1, 2].Value := 1200.00;
    // Die Lookup-Range deckt Spalte B ab, und diese Formel lebt in ihr
    Sheet.Cells[7, 2].Formula := 'VLOOKUP(A1,B:B,1)';

    case Book.Recalculate of
      lxOk:
        // Vor v2.361.98 war dieser Zweig für dieses Blatt unerreichbar
        SaveReport(Book);
      lxErrorRef:
        LogWarning('Genuine circular reference - review model inputs');
    end;
  finally
    Book.Free;
  end;
end;

Was man aufgibt, wenn Scan-Kanten aus der Ordnung ausgeschlossen bleiben

Genau eine Sache, und sie lohnt sich, klar auszusprechen statt zu verstecken. Weil Scan-Kanten nicht an der topologischen Ordnung teilnehmen, kann eine Lookup-Formel im selben Durchlauf ausgewertet werden, bevor manche Zellen in ihrer Lookup-Range neu berechnet wurden, und sie liest dann deren vorherige Werte. Das Ergebnis konvergiert bei der nächsten Neuberechnung

Das ist akzeptabel, denn es ist, was Excel tut. Für eine Arbeitsmappe ohne aktivierte iterative Berechnung ist Excels eigene Antwort auf einen Wert, der im aktuellen Durchlauf noch nicht neu berechnet wurde, der zuletzt berechnete Wert, also trifft eine Engine, die dieses Verhalten reproduziert, die Referenzimplementierung, statt sie zu approximieren. Wer eine wirklich konvergierte Antwort über ein selbstreferenzielles Modell braucht, für den ist der Mechanismus dafür die iterative Berechnung mit einer expliziten Iterationsgrenze, behandelt in dem Artikel zur iterativen Berechnung, und sie gilt für echte Zyklen statt für Scan-Überlappungen

Die Regressionsgefahr, die im Fix verborgen liegt

LookupScan zu TXLSDepRange hinzuzufügen, brachte ein Risiko mit sich, das nichts mit Lookups zu tun hat und alles mit Pascal. TXLSDepRange ist ein nicht verwalteter Datensatz, also ist eine lokale Variable dieses Typs nicht null-initialisiert. Jede Stelle in der Codebasis, die einen von Hand baut, einschließlich der Data-Table-Abhängigkeitsblöcke und mehrerer Test-Helfer, musste daher aktualisiert werden, um das neue Feld explizit zu setzen. Verpasst man eine, entscheidet das Byte, das zufällig auf dem Stack lag, ob diese Referenz als Scan-Kante behandelt wird, was einen Neuberechnungs-Bug produziert, der mit unzusammenhängenden Codeänderungen erscheint und verschwindet

// Ein neues Boolean-Feld in einem nicht verwalteten Datensatz macht jede
// manuelle Baustelle zu einem latenten Bug. Zwei sichere Idiome:
var
  R: TXLSDepRange;
begin
  FillChar(R, SizeOf(R), 0);      // alles nullen, dann eintragen
  R.Sheet1 := SheetIndex;
  R.Sheet2 := SheetIndex;
  R.Row1 := Row; R.Col1 := Col;
  R.Row2 := Row; R.Col2 := Col;

  // oder jedes Feld, auch das neue, an jeder Stelle setzen
  R.LookupScan := False;
end;

Die allgemeine Regel, die das eingebracht hat: Ein Feld zu einem Datensatz hinzuzufügen, der an mehr als einer Handvoll Stellen auf dem Stack konstruiert wird, ist eine riskantere Änderung, als sie aussieht, und der Compiler hilft Ihnen nicht, die Stellen zu finden. Ist der Datensatz von einem Hot Path erreichbar, ziehen Sie einen Helfer vor, der ihn vollständig initialisiert, statt jeder Aufrufstelle zu vertrauen, dass sie aktualisiert wurde

Einen echten Zyklus von einer Scan-Überlappung unterscheiden

Nichts an dieser Änderung schwächt die Zykluserkennung. =B7+1 in B7 ist weiterhin ein Zyklus, eine Kette von drei Formeln, die sich selbst schließt, ist weiterhin ein Zyklus, und beide werden weiterhin über das Recalculation-Ergebnis gemeldet, wobei die Zyklusmitglieder ihre vorherigen gecachten Werte behalten, während alles außerhalb des Zyklus aktuell bleibt. Geändert hat sich nur, dass das Lookup-Array-Argument keine Zyklen mehr fabriziert, die Excel nicht sieht

Wer eine Arbeitsmappe auditiert und wissen will, welche Referenzen die Engine tatsächlich aufgelöst hat und in welcher Reihenfolge, für den ist der Auswertungs-Tracer das Werkzeug; der Artikel zum Formelauswertungs-Tracer erklärt, wie man seine Ausgabe liest. HotXLS ist eine native Delphi- und C++Builder-Tabellenkomponente, die XLS, XLSX, ODS und CSV liest und schreibt ohne installiertes Excel, und die Recalculation-Engine ist in jedem Format dieselbe; die aktuelle Funktions- und Engine-Abdeckung listet die Produktseite der HotXLS Delphi Tabellenkomponente