Loading src/crypto/aes.cpp +16 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,22 @@ #define AESNI_FUNC #endif // F16 (Performance-Report): bewusst NICHT auf VAES/AVX-512 (VAES+VPCLMULQDQ, // 2-4 AES-Blöcke pro Instruktion statt 1) umgestellt, obwohl der Report dort // ein weiteres, deutliches Vielfaches gegenüber dem hier bereits implemen- // tierten AES-NI-Pfad (128-bit, 4-Block-Interleaving mit fusioniertem // GHASH — siehe aesni_gcm_ctr_ghash_128/256 und ihre CTR+GHASH-fusionierte // Decrypt-Variante) sieht. Gründe: (1) VAES ist erst ab Ice Lake/Zen 3 // verfügbar — ein separater, zur Laufzeit über CPUID erkannter Codepfad // wäre nötig, der auf älterer/virtualisierter Hardware nie greift und dort // nur als zusätzlicher, ungetesteter Pfad mitgeführt würde; (2) diese // Sandbox hat keine VAES-fähige CPU, der Pfad könnte hier also nicht // einmal funktional (geschweige denn per Benchmark) verifiziert werden — // ein reines "sollte laut Intel-Manual so funktionieren" wäre für einen // AEAD-Kernpfad einer Krypto-Bibliothek keine ausreichende Verifikation. // Der bereits vorhandene 128-bit/4-Block-Pfad bleibt der Baseline-Fallback // und deckt jede AES-NI-fähige CPU ab. static bool detect_aesni_pclmul() { #ifdef _MSC_VER int info[4]; Loading src/crypto/curve25519.cpp +18 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,24 @@ namespace netplus { // ============================================================================ // FieldElement: gf[16] like TweetNaCl (internal only) // // F18 (Performance-Report): bewusst NICHT auf eine Radix-51-Darstellung // (5 Limbs à 51 Bit, wie in donna/ref10-artigen X25519-Implementierungen) // umgestellt, obwohl diese pro scalarmult() spürbar weniger Limb-Operationen // braucht als dieses TweetNaCl-Layout mit 16 Limbs à 16 Bit. Gründe: (1) // X25519 läuft hier ausschließlich im TLS-1.3-Handshake (ein scalarmult_base // + ein scalarmult pro Verbindungsaufbau, siehe tls.cpp) — nicht im // Datenpfad, der pro Paket läuft (das ist AES-GCM, siehe aes.cpp). Der // Gewinn wäre also auf die Handshake-Rate begrenzt, nicht auf den // Durchsatz einer laufenden Verbindung. (2) Eine Radix-51-Umstellung ist // eine vollständige Neuimplementierung der Feldarithmetik (mul/square/ // carry/invert), nicht eine lokale Optimierung — jede einzelne Operation // unten müsste neu geschrieben und gegen Testvektoren verifiziert werden, // bei einem Fehler in der Trägerarithmetik einer Krypto-Bibliothek mit // stillem, schwer zu findendem Korrektheitsrisiko (falsche Shared-Secrets // nur unter bestimmten Skalaren/Bit-Mustern). Angesichts von (1) steht der // Aufwand in keinem Verhältnis zum tatsächlichen Nutzen für diesen // Anwendungsfall. // ============================================================================ class FieldElement { Loading src/event/epoll.cpp +17 −0 Original line number Diff line number Diff line Loading @@ -910,6 +910,23 @@ namespace netplus { CPU_SET(i % std::max(1, static_cast<int>(std::thread::hardware_concurrency())), &cpuset); pthread_setaffinity_np(tid, sizeof(cpuset), &cpuset); } // F28 (Performance-Report): bewusst KEINE NUMA-Awareness ergänzt // (Worker-Threads gezielt auf Cores einer NUMA-Node pinnen + // deren Verbindungszustand/Puffer aus Speicher dieser Node // allozieren, z.B. via numa_alloc_onnode()). Gründe: (1) das // hängt vollständig von der Zielmaschine ab (Anzahl NUMA-Nodes, // Core-zu-Node-Zuordnung) — eine sinnvolle Implementierung // bräuchte zur Laufzeit erkannte Topologie (libnuma) statt der // hier bereits vorhandenen einfachen `i % core_count`-Zuordnung, // und zusätzlich einen abweichenden Allokator-Pfad für // NUMA-Systeme, den diese Bibliothek bisher nirgends hat; (2) // ohne eine echte Mehrsockel-/Mehr-Node-Maschine lässt sich der // eigentliche Nutzen (weniger Remote-Memory-Zugriffe über den // Interconnect) hier nicht messen — diese Sandbox ist eine // einzelne Single-Node-Maschine, auf der jede NUMA-Node-Zuordnung // ohnehin auf Node 0 münden würde. Ein blind implementierter, // unverifizierter NUMA-Pfad wäre schlechter als gar keiner. threadpool.push_back(tid); } Loading src/quic.cpp +16 −0 Original line number Diff line number Diff line Loading @@ -81,6 +81,22 @@ ThreadPool& quicDispatchPool() { static ThreadPool pool((std::max<unsigned>)(4, std::thread::hardware_concurrency() * 2)); return pool; } // F32 (Performance-Report): bewusst NICHT auf mehrere unabhängige, // z.B. nach Connection-ID gehashte Teil-Pools ("Sharding") aufgeteilt, wie // vom Report als Weg vorgeschlagen, die eine gemeinsame Warteschlange // dieses Pools als Kontentionspunkt bei sehr vielen gleichzeitig // promovierten Verbindungen (siehe scheduleDispatches()'s Kommentar) zu // entschärfen. Grund: pro Verbindung läuft ohnehin höchstens ein Runner // gleichzeitig (das exakt einhält scheduleDispatches()'s eigene FIFO- // Garantie), sodass die geteilte Warteschlange nur beim Enqueue/Dequeue // selbst kurz gesperrt wird, nicht für die Dauer eines ganzen Callbacks — // eine reale Kontention auf dieser Warteschlange ließe sich nur mit sehr // vielen gleichzeitig promovierten (also bereits als "langsam" markierten) // Verbindungen und entsprechend hoher Kernzahl nachweisen. Diese Sandbox // hat weder die Kernzahl noch ein Lastprofil, um eine solche Kontention // überhaupt zu erzeugen, geschweige denn eine Sharding-Umstellung dagegen // zu verifizieren — eine blind hinzugefügte Hash-Sharding-Schicht wäre // hier nicht mehr als eine unbelegte Vermutung. } // namespace // ============================================================================ Loading src/socket.h +39 −2 Original line number Diff line number Diff line Loading @@ -1534,10 +1534,27 @@ namespace netplus { std::vector<PendingDispatch> dispatches); void runDispatchQueue(std::shared_ptr<quic> self); std::atomic<bool> _dispatch_promoted{false}; std::mutex _dispatch_queue_mutex; // F50: _dispatch_promoted is polled by whichever thread just // finished processing a packet (deciding inline-vs-pool dispatch) // while _dispatch_queue_mutex/_dispatch_queue/_dispatch_running are // concurrently touched by the pool worker thread draining the // queue — two independent synchronization points, each hit from a // different core, that used to sit close enough to share (or // border) a cache line with each other and with unrelated hot // connection state declared around them. Every write to one would // bounce the line and stall a concurrent read/write of the other // even though the two are logically unrelated. Padding each group // out to its own 64-byte boundary avoids that false-sharing // traffic. 64 covers every mainstream x86_64/ARM64 L1 line size; // std::hardware_destructive_interference_size was deliberately // not used here to avoid its libstdc++ ABI-tuning warning. alignas(64) std::atomic<bool> _dispatch_promoted{false}; char _dispatch_promoted_pad[64 - sizeof(std::atomic<bool>)]; alignas(64) std::mutex _dispatch_queue_mutex; std::deque<std::vector<PendingDispatch>> _dispatch_queue; std::atomic<bool> _dispatch_running{false}; char _dispatch_queue_pad[64]; // Handshake timeout tracking // Detects when client sends incomplete ClientHello with large gaps (likely packet loss) Loading @@ -1557,6 +1574,26 @@ namespace netplus { static constexpr int HS_RETRANSMIT_MAX = 5; static constexpr int HS_RETRANSMIT_INTERVAL_MS = 200; // Initial retransmit timer // F33 (Performance-Report): bewusst NICHT auf einen Ring-Buffer // (nach Packet-Number indiziert, O(1) amortisiert) umgestellt, wie // vom Report als Alternative zu diesem std::map (O(log n) je // Lookup/Insert/Erase) vorgeschlagen. Innerhalb dieses Umsetzungs- // zyklus wurden die beiden konkret gemessenen Kostenstellen bereits // gezielt behoben — F01 (recordSentPacket() nutzt jetzt einen // Buffer-Freelist statt pro Paket neu zu allokieren) und F36 // (checkLossAndRetransmit() bricht früh ab, sobald keine // verlorenen Pakete mehr folgen können, statt immer die gesamte // Map zu durchlaufen). Ein vollständiger Wechsel der Datenstruktur // (mit den Konsequenzen für Lücken durch Paketverlust, PN-Space- // Rollover-Handling und alle ~15 Erase-Stellen, siehe // _sent_data_pool-Kommentar) ist ein Aufwand, der eine dedizierte // Umsetzung mit eigener Testabdeckung verdient, statt ihn beiläufig // an diesem bereits sehr umfangreichen Umsetzungszyklus // anzuhängen — die verbleibende Verbesserung gegenüber den beiden // bereits behobenen Kostenstellen ist zudem in der Praxis klein // (std::map-Operationen sind hier durch das Betriebssystem/die // AEAD-Kosten pro Paket ohnehin dominiert, nicht durch die // Baumtiefe bei realistischen In-Flight-Fenstergrößen). // Application packet retransmission (loss detection + recovery) std::map<uint64_t, SentPacket> _sent_packets; // pn → metadata uint64_t _largest_acked_pn = UINT64_MAX; // largest packet number ACKed Loading Loading
src/crypto/aes.cpp +16 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,22 @@ #define AESNI_FUNC #endif // F16 (Performance-Report): bewusst NICHT auf VAES/AVX-512 (VAES+VPCLMULQDQ, // 2-4 AES-Blöcke pro Instruktion statt 1) umgestellt, obwohl der Report dort // ein weiteres, deutliches Vielfaches gegenüber dem hier bereits implemen- // tierten AES-NI-Pfad (128-bit, 4-Block-Interleaving mit fusioniertem // GHASH — siehe aesni_gcm_ctr_ghash_128/256 und ihre CTR+GHASH-fusionierte // Decrypt-Variante) sieht. Gründe: (1) VAES ist erst ab Ice Lake/Zen 3 // verfügbar — ein separater, zur Laufzeit über CPUID erkannter Codepfad // wäre nötig, der auf älterer/virtualisierter Hardware nie greift und dort // nur als zusätzlicher, ungetesteter Pfad mitgeführt würde; (2) diese // Sandbox hat keine VAES-fähige CPU, der Pfad könnte hier also nicht // einmal funktional (geschweige denn per Benchmark) verifiziert werden — // ein reines "sollte laut Intel-Manual so funktionieren" wäre für einen // AEAD-Kernpfad einer Krypto-Bibliothek keine ausreichende Verifikation. // Der bereits vorhandene 128-bit/4-Block-Pfad bleibt der Baseline-Fallback // und deckt jede AES-NI-fähige CPU ab. static bool detect_aesni_pclmul() { #ifdef _MSC_VER int info[4]; Loading
src/crypto/curve25519.cpp +18 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,24 @@ namespace netplus { // ============================================================================ // FieldElement: gf[16] like TweetNaCl (internal only) // // F18 (Performance-Report): bewusst NICHT auf eine Radix-51-Darstellung // (5 Limbs à 51 Bit, wie in donna/ref10-artigen X25519-Implementierungen) // umgestellt, obwohl diese pro scalarmult() spürbar weniger Limb-Operationen // braucht als dieses TweetNaCl-Layout mit 16 Limbs à 16 Bit. Gründe: (1) // X25519 läuft hier ausschließlich im TLS-1.3-Handshake (ein scalarmult_base // + ein scalarmult pro Verbindungsaufbau, siehe tls.cpp) — nicht im // Datenpfad, der pro Paket läuft (das ist AES-GCM, siehe aes.cpp). Der // Gewinn wäre also auf die Handshake-Rate begrenzt, nicht auf den // Durchsatz einer laufenden Verbindung. (2) Eine Radix-51-Umstellung ist // eine vollständige Neuimplementierung der Feldarithmetik (mul/square/ // carry/invert), nicht eine lokale Optimierung — jede einzelne Operation // unten müsste neu geschrieben und gegen Testvektoren verifiziert werden, // bei einem Fehler in der Trägerarithmetik einer Krypto-Bibliothek mit // stillem, schwer zu findendem Korrektheitsrisiko (falsche Shared-Secrets // nur unter bestimmten Skalaren/Bit-Mustern). Angesichts von (1) steht der // Aufwand in keinem Verhältnis zum tatsächlichen Nutzen für diesen // Anwendungsfall. // ============================================================================ class FieldElement { Loading
src/event/epoll.cpp +17 −0 Original line number Diff line number Diff line Loading @@ -910,6 +910,23 @@ namespace netplus { CPU_SET(i % std::max(1, static_cast<int>(std::thread::hardware_concurrency())), &cpuset); pthread_setaffinity_np(tid, sizeof(cpuset), &cpuset); } // F28 (Performance-Report): bewusst KEINE NUMA-Awareness ergänzt // (Worker-Threads gezielt auf Cores einer NUMA-Node pinnen + // deren Verbindungszustand/Puffer aus Speicher dieser Node // allozieren, z.B. via numa_alloc_onnode()). Gründe: (1) das // hängt vollständig von der Zielmaschine ab (Anzahl NUMA-Nodes, // Core-zu-Node-Zuordnung) — eine sinnvolle Implementierung // bräuchte zur Laufzeit erkannte Topologie (libnuma) statt der // hier bereits vorhandenen einfachen `i % core_count`-Zuordnung, // und zusätzlich einen abweichenden Allokator-Pfad für // NUMA-Systeme, den diese Bibliothek bisher nirgends hat; (2) // ohne eine echte Mehrsockel-/Mehr-Node-Maschine lässt sich der // eigentliche Nutzen (weniger Remote-Memory-Zugriffe über den // Interconnect) hier nicht messen — diese Sandbox ist eine // einzelne Single-Node-Maschine, auf der jede NUMA-Node-Zuordnung // ohnehin auf Node 0 münden würde. Ein blind implementierter, // unverifizierter NUMA-Pfad wäre schlechter als gar keiner. threadpool.push_back(tid); } Loading
src/quic.cpp +16 −0 Original line number Diff line number Diff line Loading @@ -81,6 +81,22 @@ ThreadPool& quicDispatchPool() { static ThreadPool pool((std::max<unsigned>)(4, std::thread::hardware_concurrency() * 2)); return pool; } // F32 (Performance-Report): bewusst NICHT auf mehrere unabhängige, // z.B. nach Connection-ID gehashte Teil-Pools ("Sharding") aufgeteilt, wie // vom Report als Weg vorgeschlagen, die eine gemeinsame Warteschlange // dieses Pools als Kontentionspunkt bei sehr vielen gleichzeitig // promovierten Verbindungen (siehe scheduleDispatches()'s Kommentar) zu // entschärfen. Grund: pro Verbindung läuft ohnehin höchstens ein Runner // gleichzeitig (das exakt einhält scheduleDispatches()'s eigene FIFO- // Garantie), sodass die geteilte Warteschlange nur beim Enqueue/Dequeue // selbst kurz gesperrt wird, nicht für die Dauer eines ganzen Callbacks — // eine reale Kontention auf dieser Warteschlange ließe sich nur mit sehr // vielen gleichzeitig promovierten (also bereits als "langsam" markierten) // Verbindungen und entsprechend hoher Kernzahl nachweisen. Diese Sandbox // hat weder die Kernzahl noch ein Lastprofil, um eine solche Kontention // überhaupt zu erzeugen, geschweige denn eine Sharding-Umstellung dagegen // zu verifizieren — eine blind hinzugefügte Hash-Sharding-Schicht wäre // hier nicht mehr als eine unbelegte Vermutung. } // namespace // ============================================================================ Loading
src/socket.h +39 −2 Original line number Diff line number Diff line Loading @@ -1534,10 +1534,27 @@ namespace netplus { std::vector<PendingDispatch> dispatches); void runDispatchQueue(std::shared_ptr<quic> self); std::atomic<bool> _dispatch_promoted{false}; std::mutex _dispatch_queue_mutex; // F50: _dispatch_promoted is polled by whichever thread just // finished processing a packet (deciding inline-vs-pool dispatch) // while _dispatch_queue_mutex/_dispatch_queue/_dispatch_running are // concurrently touched by the pool worker thread draining the // queue — two independent synchronization points, each hit from a // different core, that used to sit close enough to share (or // border) a cache line with each other and with unrelated hot // connection state declared around them. Every write to one would // bounce the line and stall a concurrent read/write of the other // even though the two are logically unrelated. Padding each group // out to its own 64-byte boundary avoids that false-sharing // traffic. 64 covers every mainstream x86_64/ARM64 L1 line size; // std::hardware_destructive_interference_size was deliberately // not used here to avoid its libstdc++ ABI-tuning warning. alignas(64) std::atomic<bool> _dispatch_promoted{false}; char _dispatch_promoted_pad[64 - sizeof(std::atomic<bool>)]; alignas(64) std::mutex _dispatch_queue_mutex; std::deque<std::vector<PendingDispatch>> _dispatch_queue; std::atomic<bool> _dispatch_running{false}; char _dispatch_queue_pad[64]; // Handshake timeout tracking // Detects when client sends incomplete ClientHello with large gaps (likely packet loss) Loading @@ -1557,6 +1574,26 @@ namespace netplus { static constexpr int HS_RETRANSMIT_MAX = 5; static constexpr int HS_RETRANSMIT_INTERVAL_MS = 200; // Initial retransmit timer // F33 (Performance-Report): bewusst NICHT auf einen Ring-Buffer // (nach Packet-Number indiziert, O(1) amortisiert) umgestellt, wie // vom Report als Alternative zu diesem std::map (O(log n) je // Lookup/Insert/Erase) vorgeschlagen. Innerhalb dieses Umsetzungs- // zyklus wurden die beiden konkret gemessenen Kostenstellen bereits // gezielt behoben — F01 (recordSentPacket() nutzt jetzt einen // Buffer-Freelist statt pro Paket neu zu allokieren) und F36 // (checkLossAndRetransmit() bricht früh ab, sobald keine // verlorenen Pakete mehr folgen können, statt immer die gesamte // Map zu durchlaufen). Ein vollständiger Wechsel der Datenstruktur // (mit den Konsequenzen für Lücken durch Paketverlust, PN-Space- // Rollover-Handling und alle ~15 Erase-Stellen, siehe // _sent_data_pool-Kommentar) ist ein Aufwand, der eine dedizierte // Umsetzung mit eigener Testabdeckung verdient, statt ihn beiläufig // an diesem bereits sehr umfangreichen Umsetzungszyklus // anzuhängen — die verbleibende Verbesserung gegenüber den beiden // bereits behobenen Kostenstellen ist zudem in der Praxis klein // (std::map-Operationen sind hier durch das Betriebssystem/die // AEAD-Kosten pro Paket ohnehin dominiert, nicht durch die // Baumtiefe bei realistischen In-Flight-Fenstergrößen). // Application packet retransmission (loss detection + recovery) std::map<uint64_t, SentPacket> _sent_packets; // pn → metadata uint64_t _largest_acked_pn = UINT64_MAX; // largest packet number ACKed Loading