Hier ist noch eine Übersicht an Features, an denen für die Zukunft gearbeitet wird.
Da sind ein paar Features dabei, die sehr viel schmerz lösen könnten. Für lean-s3 experimentiere ich gerade damit, einen Parser schnell zu bekommen. Ein langsamer Teil davon ist das Scanning. Für den hab ich jetzt schon viel rumgehackt und ihn auf .indexOf
portiert, wo es möglich war. Der Hintergrund dafür ist, dass .indexOf
unter der Haube (sehr wahrscheinlich) Vektorinstruktionen und generell eine sehr intelligente String-Suche verwendet, die man mit einem einfachen while-(x)-i++
-Loop nicht hinbekommt.
Die Daten kommen aber aus undici, entweder als JS-String oder als Buffer. Wenn man jetzt vor dieser Scec WASM zum Scannen/Parsen verwenden wollte, hätte man vor dem Scannen die Daten in den WASM-Memory-Space manuell kopieren müssen. Ein Memcopy der größe O(n) hätte denke ich sämtliche Vorteile durch schnelleres Scannen wiede rzunichte gemacht, deshalb hab ich das gar nicht erst probiert.
Mit diesen neuen Specs (speziell “multiple Memories” und “JS Strings”) gibt es hier 2 neue Möglichkeiten:
1. JS-String-Referenz übergeben
Den Result-String der HTTP-Antwort aus undici als JS-String an WASM übergeben und dann mit den String-Builtins den String verarbeiten. Damit macht man dann effektiv noch das gleiche wie vorher, aber der “Klebecode” zwischen den .indexOf
-Calls ist dann etwa schneller, da der nicht mehr auf JS-Quriks (siehe unten) prüfen muss.
2. Den Buffer als Memory übergeben
Man kann den Buffer (nicht den String) von Undici direkt als zusätzlichen Memory an WASM reichen. Das wär ebenfalls Zero-Copy.
Vorteil gebenüber 1.: Die Antwort wird höchstwahrscheinlich UTF-8 sein. JS selbst benutzt für Strings intern UTF-16, d.h. wenn man eine HTTP-Antwort bekommt, muss undici aus dem UTF-8 erstmal noch UTF-16 machen (das Problem hatte ASP.NET auch bei C# und sie haben das über das neue System.Text.Json gefixt). Das UTF-16 verbraucht unnötig viel Speicher, was schlecht für Cache-Lokalität ist. Wenn man den Buffer direkt verarbeitet, kann man sich das Re-Encoden sparen.
Ein weiteret Vorteil: Man kann dann die String-Operationskonstrukte der jeweiligen WASM-Quellsprache verwenden. Die kennt der Compiler dann schon. D.h. statt das .charCodeAt
über die JS-Builtin-Strings zu callen, verwendet man einfach das, was die jeweilige Sprache sowieso schon hat. Da diese Boundary dann nicht vorhanden ist, kann der Compiler hier sehr viel besser optimieren, vorallem über Funktionsgrenzen hinaus. Außerdem wird der Overhead deutlich geringer sein (bzw. nicht vorhanden).
2 hat aber auch einen Nachteil gegenüber 1: Da für das Decoding verschiedene APIs verwendet werden, kann hier eine Divergenz durch verschiedene Implementierungen entstehen. Z.B. könnte sich das charCodeAt
in JS anders verhalten als das aus bspw. Rust, wenn der zu lesende Buffer ungültiges UTF-8 hatte. Das könnte darin enden, dass die Rust-Version dann falsche String-Offsets beim Scannen zurück gibt.
Wenn das Stable in Node.js ist, hat man ne Menge zum Spielen. Vielleicht könnten die Memories auch für WASM-auf-Microcontroller verwendet werden, wo es verschiedene Speicherregionen gibt, die beim Microcontroller verschiedene Zwecke haben. Oder vielleicht sogar für Memory-Mapped-IO?
Edit:
So ganz 0-Copy wäre Methode 2 natürlich doch noch nicht. Undici kopiert die Daten ja aus dem Lesebuffer im Network-Code in den aggregierten Buffer, der am ende zurückgegeben wird. In C# hat man das mit dem Primitive ReadOnlySequence<T>
und Systen.IO.Pipelines
gelöst. Das ist im Prinzip eine API, bei dem eine ReadOnlySequence eine Verkettung der Buffer ist, die Undici mit den Daten aus dem Netzwerk füllt. Die Pipeline bietet dann eine Abstraktion, um aus dieser Bufferkette angenehmer zu lesen. So kann man wirklich sehr einfach parsen, ohne die gesamte Antwort in einen Buffer zu tun. Das müsste Undici aber auch supporten und das wär denke ich auch sinnvoll, wenn das eine JS-API wird, die intern nativ arbeitet.