Skip to content

SwiftQUIC: PERF: Reduce CPU by 63% parsing QUIC header - #60

Open
agnosticdev wants to merge 5 commits into
mainfrom
agnosticdev/QUICParseHeader
Open

SwiftQUIC: PERF: Reduce CPU by 63% parsing QUIC header#60
agnosticdev wants to merge 5 commits into
mainfrom
agnosticdev/QUICParseHeader

Conversation

@agnosticdev

Copy link
Copy Markdown
Collaborator

Parsing QUIC headers with Deserializer.deserialize uses a lot more CPU than expected for what should be a very fast operation. Parsing the firstOctet and short headers are by far the common cases when parsing QUIC packets, and the paths that should be the most performant. This change is to provide a fast-path option for these operations and cut the CPU usage here by 63% when parsing QUIC headers in the QUICTransfer benchmark.

Savings of about 260 megacycles.

Current top of tree:

417.04 M 52.8%  -                                             PacketParser.parseHeader(frame:dcidLength:)   
417.04 M 52.8%  30.48 M                                        specialized PacketParser.parseHeader(frame:dcidLength:)  
153.60 M 19.5%  12.00 M                                         PacketParser.parseShortHeader(frame:firstOctet:dcidLength:originalLength:)  
107.87 M 13.7%  5.00 M                                           static Deserializer<>.deserialize<>(_:claim:_:)    
41.72 M  5.3%  5.00 M                                            Frame.claim(fromStart:fromEnd:adjustSingleIPAggregate:)   
27.11 M  3.4%  27.11 M                                            0x192be0ce8 (libsystem_pthread.dylib +0x1ce9) <56F332B8-6B78-3DB7-A58C-F3F4EC03D383> 
5.00 M   0.6%  5.00 M                                             type metadata accessor for Logger    
2.60 M   0.3%  2.60 M                                             Frame.startOffset.setter 
2.00 M   0.3%  2.00 M                                             DYLD-STUB$$type metadata accessor for Logger 
38.15 M  4.8%  -                                                 static Deserializer<>.deserialize<>(_:_:) 
23.00 M  2.9%  22.00 M                                           Frame.bytes.getter    
26.73 M  3.4%  -                                                QUICConnectionID.init(storage:size:)   
26.73 M  3.4%  9.13 M                                            specialized QUICConnectionID.init(storage:size:)  
7.00 M   0.9%  -                                                Packet.init(destinationConnectionID:headerLength:spin:)    
133.11 M 16.9%  -                                               specialized UniqueDeque<>.reserveCapacity(_:)   
133.11 M 16.9%  -                                                specialized RigidDeque<>.reserveCapacity(_:)   
133.11 M 16.9%  -                                                 specialized RigidDeque<>.reallocate(capacity:)    
133.11 M 16.9%  4.57 M                                             specialized _UnsafeDequeHandle<>.reallocate(capacity:)   
99.84 M  12.6%  10.00 M                                         static Deserializer<>.deserialize<>(_:claim:_:) 
50.82 M  6.4%  9.00 M                                           Frame.claim(fromStart:fromEnd:adjustSingleIPAggregate:)    
34.82 M  4.4%  34.82 M                                           0x192be0ce8 (libsystem_pthread.dylib +0x1ce9) <56F332B8-6B78-3DB7-A58C-F3F4EC03D383>  
4.00 M   0.5%  4.00 M                                            Frame.startOffset.setter  
3.00 M   0.4%  3.00 M                                            DYLD-STUB$$type metadata accessor for Logger  
23.00 M  2.9%  22.00 M                                          Frame.bytes.getter 
1.00 M   0.1%  -                                                 specialized UniqueArray<>.span.getter 
12.01 M  1.5%  -                                                static Deserializer<>.deserialize<>(_:_:)  
12.01 M  1.5%  3.01 M                                            specialized static Deserializer<>.deserialize(_:_:)   
4.00 M   0.5%  4.00 M                                           type metadata accessor for Logger

And with this change:

158.20 M 47.8%  20.74 M                                           specialized PacketParser.parseHeader(frame:dcidLength:)  
120.46 M 36.4%  149.52 k                                            PacketParser.parseShortHeader(frame:firstOctet:dcidLength:originalLength:)  
120.31 M 36.3%  50.91 M                                              specialized PacketParser.parseShortHeader(frame:firstOctet:dcidLength:originalLength:) 
44.43 M  13.4%  11.91 M                                               Frame.copyInto(inlineArray:length:)   
9.00 M   2.7%  9.00 M                                                Frame.unclaimedLength.getter  
7.00 M   2.1%  -                                                     static InlineArray<>.empty.getter 
5.00 M   1.5%  5.00 M                                                Frame.claim(fromStart:fromEnd:adjustSingleIPAggregate:)   
3.97 M   1.2%  -                                                     Packet.init(destinationConnectionID:headerLength:spin:)   
16.00 M  4.8%  -                                                   Frame.firstOctet.getter 
1.00 M   0.3%  -                                                   Frame.claim(fromStart:fromEnd:adjustSingleIPAggregate:) 

@tfpauly

tfpauly commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Is there an approach we can use to make the actual deserialize operation cheaper here, as opposed to avoiding using it?

@agnosticdev

Copy link
Copy Markdown
Collaborator Author

Is there an approach we can use to make the actual deserialize operation cheaper here, as opposed to avoiding using it?

I created a new type called InlineDeserializer that extracts bytes from the Frame's NetworkUniqueArray<UInt8>, this is the most performant way to extract bytes from the frame. This should get us where we want to be CPU-wise.

@agnosticdev

Copy link
Copy Markdown
Collaborator Author

Its also worth mentioning why I created a new type here (InlineDeserializer) instead of using the Deserializer type directly. The Deserializer type loads the Span<UInt8> from a frame into the Span of the Deserializer. Then when values are extracted from the Deserializer it reads them from this span. This path uses too much CPU. Extracting the bytes directly from the NetworkUniqueArray<UInt8> is by far the more performant route. After this PR I will plan to move more of our data path packet parsing to use this technique.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants