1 Punkte von GN⁺ 2023-11-06 | 1 Kommentare | Auf WhatsApp teilen
  • Dave Cheneys Vortrag auf der GopherCon Singapore 2023 behandelt den Entwurfsprozess für einen Streaming-JSON-Parser in Go, der eine API ähnlich encoding/json beibehält, dabei aber den Durchsatz erhöht und Allokationen reduziert
  • JSON hat keine Längenangabe, daher muss die Eingabe bis zum Ende gelesen werden; die untere Performance-Grenze liegt mindestens bei read(N)+parse(N), weshalb das Reduzieren erneuter Besuche von Bytes und Tokens, von Kopien, Allokationen und Funktionsaufrufen im Hot Path die zentrale Einschränkung ist
  • encoding/json.Decoder.Token gibt Tokens als interface{} zurück und ist damit bequem, aber konkrete Werte escapen auf den Heap, erzeugen Allokationen proportional zur Token-Anzahl und verursachen selbst bei einem einzelnen "hello"-Token 3 allocs/op
  • pkg/json reduziert die Kosten im Hot Path durch NextToken, das Sub-Slices des Eingabe-[]byte zurückgibt, ein Sliding Window in byteReader, manuelles Inlining, direkte Aufrufe von Zustandsmethoden und das Entfernen von Bounds Checks
  • Am Ende tokenisiert pkg/json.Scanner bei bereitgestelltem Buffer ohne Allokationen; Decoder.Token ist 2- bis 3-mal schneller als encoding/json.Decoder.Token, und das allokationsärmere Decoder.NextToken ist 8- bis 10-mal schneller

Ziele und grundlegende Einschränkungen

  • Ziel ist es, als Fallstudie für Go-Paketdesign einen performanten JSON-Parser zu bauen
  • Es gibt drei Designziele
    • Unterstützung für Streaming-Verarbeitung, ohne die gesamte Eingabe in den Speicher zu laden
    • Angemessene Kompatibilität mit der High-Level-API json.Decoder aus encoding/json, bei höherem Durchsatz und weniger Allokationen
    • Zusätzlich zur encoding/json-API effizientere APIs ohne Allokationen oder mit begrenzten Allokationen anbieten
  • Wenn die gesamte Eingabe zunächst im Speicher gepuffert wird, entstehen Verfügbarkeitsrisiken, wenn die Eingabegröße unbekannt oder unendlich ist; außerdem steigt die Wartezeit vor der Verarbeitung
  • Streaming-Lesen verarbeitet Daten, sobald sie eintreffen, und kann Lesen und Verarbeitung überlappen

Zeitkomplexität beim Parsen von JSON

  • JSON hat keinen Längenmarker; um zu wissen, wie viel gelesen werden muss, muss die gesamte Eingabe gelesen werden
  • Um das 1.000. Element eines JSON-Arrays zu parsen, müssen auch die 999 Elemente davor gelesen und verarbeitet werden; die Eingabeverarbeitung lässt sich also nicht überspringen
  • Die untere Performance-Grenze ist proportional zur Eingabegröße; da nicht nur gelesen wird, sondern auch eine JSON-Zustandsmaschine durchlaufen werden muss, um Anfang und Ende von Tokens zu finden, beträgt sie mindestens read(N)+parse(N)
  • Maßstäbe zur Reduzierung zusätzlicher Kosten sind:
    • Wenn N Bytes gelesen wurden, sollte jedes Byte möglichst nur einmal verarbeitet werden
    • Dasselbe Token sollte ebenfalls nur einmal verarbeitet werden
    • Die Zahl der Funktionsaufrufe im Hot Path von Scanner oder Decoder sollte auf O(tokens) statt O(bytes) begrenzt werden
    • Kopien sollten reduziert werden, um die Anzahl erneuter Besuche derselben Bytes zu senken
    • Allokationen sollten reduziert werden, um Heap-Allokationen, Zugriffe auf gemeinsame Datenstrukturen, Locks, Cache-Contention und GC-Kosten zu senken

Tokenisierung und API-Design

  • Ein JSON-Decoder besteht grob aus zwei Stufen
    • Einem Scanner oder Tokenizer, der einen Byte-Stream in einen Stream von JSON-Tokens umwandelt
    • Einem Unmarshaller, der den JSON-Token-Stream auf Go-Objekte anwendet
  • encoding/json.Decoder.Token gibt Tokens als interface{} zurück
    • Strings werden als string, Zahlen als float64, Booleans als bool, null als nil und Trennzeichen als json.Delim dargestellt
    • Dieser Ansatz ist bequem, weil er Token-Wert und -Typ gemeinsam darstellt
  • Bequemlichkeit hat ihren Preis
    • Brad Fitzpatrick nennt die Token-API eine garbage factory
    • Durch das Design der Decoder.Token-API escapen die konkreten Werte, die jedem Token zugeordnet werden, auf den Heap
    • Die Anzahl der Allokationen ist an die Anzahl der Tokens in der Eingabe gebunden
  • In einem Benchmark mit einem einzelnen "hello"-Token zeigt encoding/json 355 ns/op, 19,7 MB/s, 37,0 B/op und 3,00 allocs/op
  • API-Design bestimmt Allokationen, und Allokationen können sich direkt auf die Performance auswirken

[]byte-Tokens und implizite Typinformationen

  • Bei JSON-Tokens lässt sich der Typ bereits am ersten Zeichen erkennen
    • {, }: Anfang und Ende eines Objekts
    • [, ]: Anfang und Ende eines Arrays
    • t: true
    • f: false
    • n: null
    • ": String
    • -, 0~9: Zahl
  • Die API Decoder.NextToken von pkg/json wandelt Eingabe-[]byte nicht in Go-Werte um, sondern gibt die Bytes, die das Token darstellen, direkt als Sub-Slice aus der Eingabe zurück
  • Das erste Byte des zurückgegebenen []byte gibt den Token-Typ an
  • Diese API hat Einschränkungen
    • Die Ausgabe ist keine Kopie, sondern ein Sub-Slice der Eingabe und hat daher eine begrenzte Lebensdauer
    • Das ähnelt der API von bufio.Scanner
    • Um Token-Typen oder konkrete String- und Zahlenwerte bequemer zu handhaben, braucht es eine Abstraktion auf höherer Ebene

Effizientes Lesen: byteReader

  • Die traditionelle Methode io.Reader.Read kopiert Daten vom Reader in einen Buffer; auch diese Kopie verursacht Kosten
  • io.Reader.Read überlässt das Buffer-Management dem Aufrufer
    • Wenn byteweise gelesen wird, kann Speicher nötig sein, um bereits gelesene Bytes zu behalten oder zurückzugehen
    • Wenn zunächst in einen großen Buffer gelesen und dann Anfang und Ende von Tokens gesucht werden, sind viel Verwaltungsaufwand, Kopien und Buffer-Erweiterungen nötig, sobald das Token-Ende nicht im Buffer liegt
  • Als Alternative wird byteReader verwendet, inspiriert von Steven Schveighoffers iopipe und Ideen von Phil Pearl
  • byteReader stellt über io.Reader ein Sliding Window bereit und ähnelt bufio.Reader, hat aber eine effizientere API
    • window() gibt das aktuelle Fenster ungelesener Daten zurück
    • release(n) verwirft die ersten n Bytes des Fensters
    • extend() liest weitere Daten aus dem darunterliegenden Reader und erweitert das Fenster
  • Der Benchmark zur Suche nach Whitespace ist eine Baseline, die jedes Zeichen besucht und nur prüft, ob es Whitespace ist; bei mehreren Eingaben erreicht sie etwa 2,04~2,07 GB/s
  • Beispielcode für den Whitespace-Zähler steht unter github.com/davecheney/whitespace

Scanner-Optimierung

  • Scanner.Next überspringt Whitespace dazwischen, bestimmt anhand des ersten Zeichens im Fenster das Token und liest bis zum Token-Ende
  • Die anfängliche Performance von Scanner.Next liegt im Vergleich zur Whitespace-Baseline bei etwa 1/4 bis 2/5
    • Beispiel: Scanner/canada 510 MB/s, citm_catalog 677 MB/s, sample 837 MB/s
  • Die erste Optimierung besteht darin, Aktualisierungen des Felds s.offset durch die lokale Variable offset zu ersetzen
    • s.offset ist beim Eintritt in die Funktion und beim Verlassen 0, daher sind interne Änderungen von außen nicht sichtbar
    • Durch die lokale Variable kann der Compiler temporäre Speicher-Schreibvorgänge vermeiden
    • citm_catalog sinkt von 2,52 ms auf 1,80 ms, eine Reduktion um 28,46 %, sample von 828 µs auf 528 µs, eine Reduktion um 36,24 %
  • Der Grund für die unterschiedliche Wirkung je nach Eingabe ist die unterschiedliche Anzahl von Whitespace-Zeichen
    • canada hat nur 33 Whitespace-Zeichen
    • citm hat 1.227.563 Whitespace-Zeichen
  • Die zweite Optimierung ist das manuelle Inlining von Scanner.token in Scanner.Next
    • Der Go-Compiler kann Scanner.token, parseString, parseNumber, Scanner.Next und andere wegen for-Schleifen und Funktionskomplexität nicht automatisch inlinen
    • Scanner.Next und Scanner.token werden pro Eingabe-Token aufgerufen, wodurch zwei Funktionsaufrufe pro Token entstehen
  • Nach manuellem Inlining verbessert sich der Durchsatz um 9~24 %
    • canada steigt von 512 MB/s auf 642 MB/s, ein Plus von 24,50 %
    • citm_catalog steigt von 960 MB/s auf 1105 MB/s, ein Plus von 15,16 %
    • sample steigt von 1,33 GB/s auf 1,46 GB/s, ein Plus von 9,11 %
  • Die Wirkung der Optimierungen lässt sich in zwei Punkten zusammenfassen
    • Aktualisierungen von s.offset werden von einmal pro Byte auf einmal pro Token reduziert
    • Das Vermeiden von Funktionsaufrufen im Hot Path kann die Performance verbessern

Validierung und Decoder.NextToken

  • Der Scanner allein kann Tokens aufteilen, aber für vollständige JSON-Verarbeitung ist Zustandsvalidierung erforderlich
  • JSON ist eine Zustandsmaschine; je nach aktuellem Token ist eingeschränkt, welches Token als Nächstes kommen darf
    • Nachdem beispielsweise {, "username" gelesen wurden, ist nur : gültig
  • Decoder.NextToken legt Zustandslogik über Scanner.Next, um zu prüfen, ob die Token-Sequenz gültig ist
  • Die Zustände umfassen unter anderem Wert, Objekt-Key-String, Objekt-Doppelpunkt, Objektwert, Objekt-Komma, Array-Wert, Array-Komma und Endzustand
  • Schon mit der anfänglichen Validierungsimplementierung zeigt pkg/json 8- bis 10-mal schnellere Ergebnisse als encoding/json
    • canada: pkg/json 399 MB/s, encoding/json 34,6 MB/s
    • citm_catalog: pkg/json 713 MB/s, encoding/json 87,1 MB/s
    • sample: pkg/json 1,23 GB/s, encoding/json 216 MB/s

Optimierung der Zustandsübergänge

  • Im Zentrum von Decoder.NextToken steht eine switch-Anweisung
  • Ein gewöhnliches switch kann wie eine Reihe von if-Anweisungen implementiert werden; eine lange Verzweigungskette teilt den Instruktionsstrom auf und belastet den Branch Predictor der CPU
  • Man könnte eine Tabelle verwenden, um aus dem Zustandswert die Zustandsmethode zu finden, aber die Beispielimplementierung kompiliert wegen der Initialisierungsschleife nicht
  • Stattdessen wird Gos method expression genutzt, um in d.state direkt eine Methode statt eines Zustands-Enums zu speichern
    • Decoder.NextToken ruft die aktuelle Zustandsmethode direkt auf, etwa mit return d.state(d, tok)
  • Dieser Computed-goto-Ansatz bringt für sich genommen keine große Performance-Verbesserung
    • Einige Eingaben ändern sich fast gar nicht, und bei twitter, code, example wird es geringfügig langsamer
    • sample wird um 1,15 % schneller
  • Diese Änderung ermöglicht die nächste Optimierung: Outlining

Outlining und Entfernen von Bounds Checks

  • Nach dem Outlining führt Decoder.NextToken nur noch return d.state(d) aus, und jede Zustandsmethode ruft direkt d.scanner.Next() auf
  • Da tok nicht mehr als Argument an die Zustandsmethode übergeben wird, werden im Call Stack 3 words eingespart
  • Weil die Prüfung len(tok) < 1 und switch tok[0] in derselben Funktion liegen, wird das Entfernen von Bounds Checks möglich
    • Zuvor lag die len(tok)-Prüfung in Decoder.NextToken, während die Zustandsmethode als Method Expression aufgerufen und nicht inlined wurde
    • Daher war für tok[0] in der Zustandsmethode ein Bounds Check nötig
    • Wenn die Längenprüfung in derselben Funktion erfolgt, kann der Compiler beweisen, dass tok mindestens Länge 1 hat
  • Auch Decoder.NextToken selbst wird einfacher und damit inline-fähig
    • Der Aufrufer sieht statt dec.NextToken() praktisch einen direkten Aufruf der aktuellen Zustandsmethode
    • Die Kosten des Funktionsaufrufs entfallen

Finale Benchmark-Ergebnisse

  • Die unterste Ebene pkg/json.Scanner führt bei einem Buffer von einigen KB Streaming-Tokenisierung ohne Allokationen aus
    • canada: 638,78 MB/s, 0 B/op, 0 allocs/op
    • citm_catalog: 1110,51 MB/s, 0 B/op, 0 allocs/op
    • sample: 1471,01 MB/s, 0 B/op, 0 allocs/op
  • pkg/json.Decoder.Token ist 2- bis 3-mal schneller als encoding/json.Decoder.Token
    • canada: 101,98 MB/s vs. 33,19 MB/s
    • citm_catalog: 333,23 MB/s vs. 82,71 MB/s
    • sample: 788,59 MB/s vs. 209,12 MB/s
  • pkg/json.Decoder.NextToken hat deutlich weniger Allokationen und ist 8- bis 10-mal schneller
    • canada: 466,52 MB/s, 136 B/op, 3 allocs/op vs. 34,42 MB/s, 17.740.399 B/op, 889.106 allocs/op
    • citm_catalog: 798,58 MB/s, 136 B/op, 3 allocs/op vs. 86,08 MB/s, 5.661.597 B/op, 324.692 allocs/op
    • sample: 1346,85 MB/s, 1144 B/op, 9 allocs/op vs. 217,44 MB/s, 723.781 B/op, 26.095 allocs/op
  • Auf der höchsten API-Ebene kann pkg/json Go-Objekte auf dieselbe Weise wie encoding/json unmarshaln
    • canada: 82,08 MB/s vs. 58,70 MB/s
    • citm_catalog: 215,66 MB/s vs. 104,00 MB/s
    • sample: 615,99 MB/s vs. 128,04 MB/s
  • Der Vortragslink ist dave.cheney.net/paste/gophercon-sg-2023.html, der Code liegt unter github.com/pkg/json

Erkenntnisse aus dem Design

  • Allokationen wirken sich auf Performance aus

    • Auch wenn der GC schnell allokiert und effizient einsammelt, ist es immer schneller, gar nicht zu allokieren
    • API-Design kann Allokationen vermeiden
    • Der Großteil der Geschwindigkeitssteigerung dieses Pakets kommt aus reduzierten Allokationen
    • Zeit, die nicht im Heap-Allokationspfad und in GC-Zyklen verbracht wird, steht fürs Scannen zur Verfügung
    • Die API encoding/json.Decoder erfordert Allokationen, weil sie primitive Werte als interface{} zurückgibt
    • Werte escapen auf den Heap und werden damit faktisch zu Zeigern auf Werte
    • Bei der Datenverarbeitung können Allokationen der größte Performance-Kostenfaktor eines Algorithmus sein
    • Das sorgfältige Reduzieren von Kosten pro Byte und pro Token ist der zweitgrößte Performance-Hebel
    • Wichtig ist die Richtung, Funktionsaufrufe pro Byte in Funktionsaufrufe pro Token zu verwandeln
    • Ausgangspunkt war die Annahme, dass encoding/json wegen seiner API langsamer sein kann; wenn man eine andere API akzeptiert, sind auf manchen Unmarshal-Pfaden 2- bis 3-fache und bei der Tokenisierung 8- bis 10-fache Performance möglich

1 Kommentare

 
GN⁺ 2023-11-06
Hacker-News-Kommentare
  • Sieht ziemlich gut aus. Im Laufe meiner Karriere habe ich schon viel zu viele JSON-Parser gebaut, aber es ist wirklich schön, eine Referenz zu haben, die Schritt für Schritt zeigt, wie man einen vernünftigen und schnellen JSON-Parser entwirft.
    Allerdings braucht JSON nicht zwingend einen expliziten Tokenizer. Man kann das Token-Konzept weglassen und Parsing und Tokenisierung vollständig zusammenlegen. So wird es normalerweise gemacht, und das Ganze wird einfacher.
    Bei Sprachen wie ECMAScript ist das deutlich schwieriger, weil es Fälle gibt, die zunächst wie eine Teilmenge der Syntax von Klammerausdrücken aussehen, etwa Arrow Functions, und erst davon abhängen, ob ein => folgt; dadurch kann ein beliebig langer Lookahead nötig werden.

    • Ich frage mich, was man beruflich gemacht haben muss, um im Laufe seiner Karriere „zu viele JSON-Parser“ gebaut zu haben.
  • Ein gut nachvollziehbarer Artikel, und er beschreibt sehr klar den Ablauf, wie man es machen würde, wenn man es selbst implementiert.
    Wenn es in der Produktionsumgebung um reine Performance geht, lohnt sich auch ein Blick auf Daniel Lemires https://github.com/simdjson/simdjson. Es gibt auch einen Go-Port von MinIO: https://github.com/minio/simdjson-go.

    • Wenn die JSON-Struktur immer gleich ist, kann man unter Umständen besser sein als ein generischer JSON-Parser.
    • Als ich früher die Performance mehrerer JSON-Parser verglichen habe, waren SIMD-basierte Parser enttäuschend langsam im Vergleich zu meinen Erwartungen.
    • Die schnellste JSON-Bibliothek in Go stammt von dem Unternehmen hinter TikTok.
    • simdjson war schon seit sehr langer Zeit nicht mehr der schnellste.
  • Was ich beim Bau schneller JSON-Parser gelernt habe, ist zwar stark von den jeweiligen Spracheigenschaften geprägt, lässt sich aber allgemein so zusammenfassen:
    Bei der Tokenisierung sollte man Heap-Allokationen vermeiden. Ein Tokenizer sollte besser ein auf dem Stack allokiertes Struct zurückgeben oder eine Funktion sein, die ein int64-Token zurückliefert, in dem Startposition, Länge, Typ-Offset usw. des Tokens gepackt sind.
    Auch beim Parsing sollte man Heap-Allokationen vermeiden und für Clients, die mit Slices auf den Buffer arbeiten wollen, eine Schnittstelle wie getString(key String) unterstützen.
    Beim Deserialisieren in Objekte, deren Felder zur Compile-Zeit bekannt sind, erzeugt man normalerweise ein switch auf Basis der Schlüssellänge, bevor man String-Werte vergleicht.
    In Daten-Pipelines, die viel JSON verarbeiten, konnte allein die Wahl der JSON-Bibliothek einen Performance-Unterschied um den Faktor 3 bis 10 ausmachen, und die großen Parser wollen im Allgemeinen Objekte allokieren.
    Wenn die zu serialisierenden und zu deserialisierenden Klassen zur Compile-Zeit bekannt sind, macht Java Jackson das ziemlich gut, aber mit sorgfältigem Coding und Profiling kann man noch ungefähr Faktor 2 herausholen.
    Wenn man dagegen beliebiges JSON verarbeitet, versuchen Mainstream-Parser viele Allokationen vorzunehmen; ein selbst geschriebener, stärker eingreifender Parser kann das vermeiden, und bei Tausenden bis Millionen Objekten pro Sekunde ist der Performance-Gewinn sehr groß.

  • Mit einem ähnlichen Ansatz habe ich einen GraphQL-Tokenizer und -Parser gebaut; auch der kommt ohne Speicherallokationen aus und ist ziemlich schnell. Wer sich den Code ansehen möchte: https://github.com/wundergraph/graphql-go-tools

    • Mein eigenes Frankenstein-Werk könnte auch interessant sein: https://github.com/graph-guard/gqlscan
      Ich habe dazu auch einen Vortrag gehalten, leider wurde er nicht aufgezeichnet. Ich bin fast verrückt geworden, als ich versucht habe, in Go möglichst viel herauszuholen :D
    • Ich frage mich, wie groß dieses Problem bei einem Allowlist-basierten GQL-Server ist, der alle Queries im Voraus kennt. Man kann die AST-Parsing-Ergebnisse cachen oder speichern, sodass es vielleicht nur in den ersten Minuten nach dem Start des Containers ein Performance-Problem ist.
      Oder ich frage mich, ob es sich auch auf andere Weise auswirkt.
  • In n2[1] brauchte ich einen schnellen Tokenizer und hatte dasselbe Problem mit dem Müllgenerator. Im Grunde war es die Mischung aus einer festen Menge von Konstanten-Tokens wie json.Delim und Strings, die Allokationen verursachen.
    Eine Lösung, die ich ziemlich gut finde, ist, den Tokenizer generisch über irgendein T zu machen und eine Funktion entgegenzunehmen, die Byte-Slices in T umwandelt, sodass man statt Strings T verwendet.
    So kann der Aufrufer eine effizientere Darstellung bereitstellen, zum Beispiel eine mit weniger Allokationen, und gleichzeitig kann man in Unit-Tests bequem die Identitätsfunktion verwenden, um den Tokenizer zu testen.
    In gewisser Weise ähnelt das dem Verschmelzen von Tokenizer und Parser zur Build-Zeit, aber dank Generics kann der Tokenizer die Schichtentrennung beibehalten, ohne die Darstellung des Parsers zu kennen.
    [1] https://github.com/evmar/n2

  • Mit besserem API-Design kann man gegenüber der Standardbibliothek Verbesserungen erzielen, aber es ist praktisch schwierig, einen vollständigen Streaming-Parser zu bauen und dabei nicht eine Struktur halb zu befüllen und dann mitten drin auszusteigen, bevor ein Fehler entdeckt wird. Die Standardbibliothek scheint auch das als explizite Design-Einschränkung zu haben.

  • Vielleicht habe ich etwas übersehen, aber der Autor sagt wiederholt, er habe einen „Streaming“-Parser gebaut, erklärt aber nicht, was das tatsächlich bedeutet.
    Insbesondere gibt es keine Erklärung dazu, wie wiederholte Schlüssel in der „Hash-Tabelle“ behandelt wurden. Ich frage mich, ob bei einem wiederholten Schlüssel der Sink-Code zweimal aufgerufen wird oder ob gewartet wird, bis die gesamte „Hash-Tabelle“ gelesen ist, bevor der Sink-Code aufgerufen wird.
    Meiner Meinung nach ist JSON hierarchisch, hat keine vorab bekannte Länge und besitzt vor allem wiederholte Schlüssel, wodurch es grundsätzlich schlecht für Streaming geeignet ist.
    Man kann bestimmte Teilmengen von JSON Streaming-freundlicher machen, aber wenn man das tut, gibt es keinen besonderen Grund, JSON zu reparieren. Wenn die Lösung darin besteht, JSON zu ändern, ist aus meiner Sicht ein ganz anderes Format besser.

  • Schön, Phil Pearl erwähnt zu sehen.
    https://github.com/bytedance/sonic ist ebenfalls einen Blick wert.

  • Es ist überraschend, dass es keine Möglichkeit gibt, bei einer Funktion, die zu groß ist und deshalb nicht inline eingebettet wird, zu sagen: „Diese Funktion wirklich inlinen“.
    Einfaches Zählen und Suchen von Whitespace-Zeichen ließe sich mit SIMD-Vektorisierung wohl deutlich beschleunigen, aber ich verstehe, dass das außerhalb des Umfangs des Autors liegt.

    • Natürlich kann man Inlining erzwingen.
  • Die Aussage „Es ist unrealistisch zu erwarten, dass man die gesamte Eingabe im Speicher halten kann“ ist für die meisten Anwendungen falsch.

    • Die meisten Anwendungen lesen JSON aus dem Netzwerk, und das ist ein Stream. Selbst wenn das JSON relativ klein ist, erhöht es die Latenz erheblich, wenn man die gesamte Anfrage im Speicher puffert und daran herumarbeitet.
    • Stimmt, aber für Anwendungen, die ETL-artige Transformationen auf großen Datensätzen durchführen müssen, ist Streaming eine enorm nützliche Strategie.
      Man könnte sagen, dass Go nicht das richtige Werkzeug dafür ist, aber mit Optimierungen dieser Art sehe ich keinen Grund, warum es nicht gehen sollte.
    • Wenn man eine Bibliothek baut, muss man die Grenzen ausdrücklich benennen oder Streaming unterstützen.
      Ich habe schon JSON-Daten im Gigabyte-Bereich hineingeschoben, daher bin ich für Streaming-Parser dankbar. Außerdem ist Streaming-Unterstützung auch ein Signal dafür, dass der Autor mehrere Anwendungsfälle kennt und besseres Engineering betreibt.
      Speicher ist nur in der Theorie billig und nahezu kostenlos, in der Realität nicht.
    • Wenn es genügt, dass es „auf die Platte passt“, ist mmap() dann nicht auch eine mögliche Option? Echte Streaming-Anforderungen, zum Beispiel wenn man frühe Daten aus einer einzelnen JSON-Datei wie einen Strom von Transaktionen oder Jobs früh verarbeiten muss, sind ein separater Fall.
    • Zählt der Body einer HTTP-Anfrage auch als Teil der Eingabe?