zplCore.comzplCore.com

36Performance

zplCore.com erzeugt über 200.000 echte Versandetiketten pro Sekunde (100 × 150 mm, 33 Elemente, QR + Code 128 + GS1-128) aus Dictionary- oder JSON-Datensätzen in eine ZPL-Datei – unter JIT und NativeAOT, gemessen auf einem Apple M3 Max (14 Kerne), .NET 10, Median aus 5 Läufen, samples/ShippingBench.

Echtes Versandetikett (samples/ShippingBench)

Gemessen am 21.10.2026: Apple M3 Max (10 Performance- + 4 Effizienzkerne), macOS 26.6, .NET 10, Release, Workstation-GC; ein Aufwärmlauf, dann der Median aus 5 Läufen. Ende zu Ende: Datensätze → ZPL → Datei auf der Platte (StreamWriter, 64-KB-Puffer). Alle Datenquellen schreiben eine byte-identische Datei mit 131 MB (vor der Messung per SHA-256 geprüft), unter JIT und NativeAOT gleichermaßen.

Das Benchmark-Etikett: Versandetikett 100 × 150 mm mit QR-Code, Code 128 und GS1-128-SSCC
Benchmark-Etikett, 203 dpi

Das Etikett: samples/templates/printjob.json, ein Versandetikett 100 × 150 mm (4 × 6 Zoll) mit 33 Elementen – Absenderblock, invertiertes Servicefeld, Empfänger (Land per Ausdruck), Routing-Code, Gewicht, Packstücke, Versanddatum (Format(ToDate([ShipDate]), "dd.MM.yyyy")), QR-Code (Tracking-URL), Code 128 (Sendungsnummer), GS1-128-SSCC, Seitenzähler, Rahmen und Linien – dazu ein Start- und ein End-Label. Die Datensätze variieren wie echte Daten (Umlaute, Doppelnamen, 9 Orte in AT/DE/CH/IT, 6 Services, 2,5–29 kg, gültige SSCCs). Downloads: Etikett als PDF / ZPL, Druckauftrag (Start + 3 Sendungen + Ende) als PDF / ZPL (300 dpi), Bild PNG 300 dpi.

200.000+Versandetiketten/s (Dictionary/JSON, JIT und NativeAOT)
< 0,5 s100.000 Versandetiketten (Dictionary 0,40 s, JSON 0,46 s)
1.000+PNG-Vorschauen/s (14 Kerne, 203 dpi)
100.000+Etiketten/s aus C#-Objekten (POCO, JIT)
Datenquelle (100.000 Etiketten + Start/Ende → ZPL-Datei)JIT Etiketten/sNativeAOT Etiketten/sZeit
IReadOnlyDictionary<string, object?>-Datensätze249.286238.5900,40–0,42 s
JSON-Datensätze (JsonElement)216.171213.6370,46–0,47 s
POCO mit Map() je Feld128.16591.7840,78–1,09 s
POCO, For<T>() (Reflection)124.99583.9940,80–1,19 s
Gespeichertes Format (^DF/^XF), POCO109.53172.1130,91–1,39 s
Rendern, 5.000 Etiketten, 14 Kerne, 203 dpi: PNG-Vorschau1.2241.182
Rendern, 5.000 Etiketten, 14 Kerne, 203 dpi: PCX (1 Bit)1.058462

Für Massendaten Dictionary- oder JSON-Datensätze übergeben: Sie bleiben unter JIT und NativeAOT über 200.000 Etiketten/s (langsamster Einzellauf 208.068/s). POCO-Bindung erreicht unter JIT rund 125.000/s, unter NativeAOT 84.000/s.

5 Millionen Datensätze und Docker (NativeAOT): Datensätze als JSON Lines (1,56 GB, ein Datensatz je Zeile), zeilenweise gelesen, als JsonElement gebunden, in eine 6,53 GB große ZPL-Datei gestreamt. Derselbe Apple M3 Max; Docker Desktop führt den Container in einer VM aus, daher ist Datei-I/O auf das Volume langsamer als nativ.

LaufEtiketten/sZeitSpitzenspeicher
NativeAOT unter macOS, 5 Millionen Datensätze132.16437,8 s248 MB RSS
Docker, NativeAOT linux-arm64, --memory 1g, 5 Millionen Datensätze91.13354,9 s90 MB
Docker, 100.000 Etiketten, Dictionary- / JSON-Datensätze157.489 / 119.2710,64 / 0,84 s
Docker, PNG-Vorschau, 14 Kerne1.164

Der Speicher bleibt bei jeder Anzahl an Datensätzen konstant (eine gestreamte Quelle wird weder gezählt noch gepuffert). Image build/Dockerfile.shippingbench: 127 MB, runtime-deps:10.0-noble-chiseled-extra, nur das native Programm, ohne Root.

dotnet run -c Release --project samples/ShippingBench                       # JIT
dotnet publish samples/ShippingBench -c Release -r osx-arm64 -p:PublishAot=true -o out   # NativeAOT
docker build -f build/Dockerfile.shippingbench -t zplcore-shippingbench .
docker run --rm -v zplcore-bench:/data zplcore-shippingbench 1000 10 1 5000000   # + 5 Millionen JSON-Lines-Datensätze
dotnet run -c Release --project samples/ShippingBench -- sample out/sample        # Etikett als PNG/PDF/ZPL

Kleines Label mit 7 Elementen (samples/PerfTest)

samples/PerfTest erzeugt 100.000 Labels in einem Druckauftrag (Start-Label + 100.000 Labels + End-Label, Schnitt nur nach dem letzten Label) – aus Objekten, gestreamt aus EF Core/SQLite und aus LiteDB. Es verwendet ein kleines Label 100 × 60 mm mit 7 Elementen (Text, Ausdruck, Zahlenformat, Code 128, Zähler), nicht das Versandetikett oben. Gemessen im Release-Build auf 14 Kernen (Apple M3 Max, .NET 10), Einzellauf.

< 1 s100.000 Labels als ZPL
14,7 MBAuftragsdatei (gespeichertes Format)
24–64 MBHeap (Streaming)
Test (100.000 Labels, ein Job)ZeitLabels/sDateiHeap
Objekte → ZPL, gespeichertes Format (^DF/^XF)0,73 s136.00014,7 MB24 MB
Objekte → ZPL, volles Format je Label0,39 s256.00029,1 MB24 MB
EF Core / SQLite AsAsyncEnumerable() → Datei0,57 s176.00014,7 MB29 MB
LiteDB-Query → Datei0,50 s202.00014,7 MB64 MB
Vorschau PNG sequenziell (799 × 480 px, Graustufen-PNG)≈ 3,4 min49018,2 MB / 2.00084 MB
Vorschau PNG parallel (14 Kerne)≈ 25 s4.03317,1 MB / 2.000106 MB

Die ZPL-Erzeugung für den Drucker ist praktisch kostenlos und streamt mit konstantem Speicher. Das gespeicherte Format halbiert die übertragene Datenmenge.

Die ZPL-Tests binden Klassen (Shipment-Objekte) und erzeugen deshalb sequenziell; Dictionary-, JSON- und DataRow-Datensätze entstehen auf allen Kernen – siehe parallele Job-Erzeugung.

Render-Optimierung

Schwarz-weiße Labels werden als 8-Bit-Graustufen-PNG (mit Kantenglättung) bzw. echtes 1-Bit-PNG (AntiAlias = false) statt 32-Bit-RGBA geschrieben (ZplRenderSettings.CompactPng, PngCompression Fastest/Optimal/SmallestSize). Einfache ^GB-Rahmen werden direkt gezeichnet, und das Label-Bild wird nur kopiert, wenn ^MC N es braucht.

Pro Label (100 × 60 mm, 203 dpi)vorherjetzt
Rendern zu Bitmap1,28 ms0,39 ms
Rendern + PNG8,19 ms1,86 ms
PNG-Größe18 KB9 KB
100.000 Vorschauen parallel2,2 min25 s

Parallele Skalierung

SkiaSharp serialisiert zwei Dinge global: Skias Glyph-Cache (jedes DrawText) und SkiaSharps Handle-Tabelle (jedes neu erzeugte SKPaint/SKFont/SKBitmap – und DrawBitmap, das intern jedes Mal ein SKImage anlegt). zplCore.com rastert deshalb jede Glyphe einmal pro Schrift/Größe in einen sperrfreien Cache aus SKImage-Masken und zeichnet Text per DrawImage; Schriften werden explizit durchgereicht statt über SKFont.Typeface abgefragt. Ergebnis gemessen mit samples/RenderBench -- 150 scale (Apple M3 Max, 10 Performance- + 4 Effizienzkerne):

ThreadsRendern + PNG vorherRendern + PNG jetztnur Text vorhernur Text jetzt
1499/s (1,0×)549/s (1,0×)5.665/s (1,0×)6.306/s (1,0×)
41.984/s (4,0×)2.225/s (4,1×)3.770/s (0,7×)28.183/s (4,5×)
82.245/s (4,5×)4.090/s (7,5×)3.162/s (0,6×)42.434/s (6,7×)
101.826/s (3,7×)5.022/s (9,2×)3.150/s (0,6×)39.384/s (6,2×)
dotnet run -c Release --project samples/RenderBench -- 150 scale [label|text|barcode|box|empty]
dotnet run -c Release --project samples/RenderBench -- 500   # Mikro-Benchmark pro Element
dotnet run -c Release --project samples/PerfTest -- 100000 perf-out

Durchsatz der Web-API

ASP.NET-Core-Minimal-API aus samples/WebApiDemo: 14 Kerne (Apple M3 Max), Server-GC, 14 Worker, kleines Label 100 × 60 mm bei 203 dpi (nicht das Versandetikett oben); Client und Server im selben Prozess.

SzenarioLabels/sMB/sp50p95GC-Heap
JSON-Payload → ZPL, 1.000 Datensätze/Request62.85720,316,2 ms16,7 ms178 MB
JSON-Payload → ZPL, 10.000 Datensätze/Request112.48436,476,1 ms136,9 ms183 MB
JSON-Payload → ZPL, 100.000 Datensätze/Request186.57260,5527,5 ms565,2 ms414 MB
DB-Streaming → ZPL, 10.000 Datensätze151.74449,169,6 ms73,1 ms361 MB
DB-Streaming → ZPL, 100.000 Datensätze199.10264,6504,3 ms505,2 ms372 MB
JSON 1k, 1 / 4 / 16 Clients196.112 / 240.453 / 155.34463,4 / 77,7 / 50,24,8 / 16,4 / 100,8 ms8,6 / 21,7 / 120,8 ms404–419 MB
DB 10k, 4 / 16 Clients215.699 / 179.86069,7 / 58,2186,2 / 878,0 ms202,4 / 907,6 ms395–408 MB
Vorschau, 50 Labels/Request: PDF 1 / 14 Clients583 / 5.0855,0 / 43,587,0 / 131,8 ms94,0 / 154,4 ms267–472 MB
Vorschau, 50 Labels/Request: PNG (zip) 1 / 14 Clients484 / 3.9954,6 / 38,1108,3 / 162,0 ms108,7 / 188,1 ms269–473 MB
Job-Queue: 256 × JSON 1k → ZPL (153 Jobs/s)153.43849,698,8 ms365,5 ms885 MB
Job-Queue: 64 × DB 10k → ZPL (18,6 Jobs/s)186.36160,32.236,5 ms3.411,1 ms828 MB
Job-Queue: 64 × JSON 50 → PDF (104 Jobs/s)5.39646,2345,2 ms445,6 ms467 MB

Die ZPL-Erzeugung sättigt bei etwa 199.000 Labels/s (ab 4 parallelen Requests); mehr Clients erhöhen nur die Latenz. Rendern kostet etwa 40× mehr und skaliert mit den Kernen. Die Queue-Latenz enthält die Wartezeit (alle Jobs auf einmal eingereicht).

Speicher und Dauertest

LastLabelsSpeicher
Dauertest, kleine Labels, parallel gerendert1.000.000RSS 128–138 MB · GC-Heap 2–3 MB, flach (kein Leck)
Dauertest, 4 × 6″-PNGs, 14 Threads1.000.000RSS 438–481 MB, flach
ZPL-Streaming aus Objekten100.000Heap 24 MB
Streaming aus EF Core / SQLite in eine ZPL-Datei100.000Heap 29 MB
Streaming aus LiteDB in eine ZPL-Datei100.000Heap 64 MB

Alle Zahlen sind Messungen mit den Benchmarks im Repository, keine Zusicherung.