SwiftQUIC: PERF: Avoid temporary frame array in the send path - #129
SwiftQUIC: PERF: Avoid temporary frame array in the send path#129agnosticdev wants to merge 4 commits into
Conversation
rnro
left a comment
There was a problem hiding this comment.
This looks like a great perf change, I think I agree with all of Tommy's comments 😄. The methods on an object which never access self. is a giveaway for these.
| public mutating func drainArrayKeepingCapacity() -> FrameArray { | ||
| let count = self.count | ||
| let returnArray = self | ||
| self = FrameArray(capacity: count) |
There was a problem hiding this comment.
count is the number elements, not the underlying capacity, is using it for the capacity intentional? Should it be self.frames.capacity instead?
There was a problem hiding this comment.
It is intentionally using count, yes. Check out the creation of lowerSendQueue, it now has a starting capacity on it too. The intention here is that these FrameArray's should maintain a capacity based on how many frames were previously in lowerSendQueue. That way the collection does not have to spend so many cycles reallocating to fit newly added frames.
There was a problem hiding this comment.
So is the assumption that the lowerSendQueue sends approximately the same number of items each time?
This doesn't feel like it would be representative of real traffic patterns. and I'd be worried about one large or small batch negatively impacting perf because the next FrameArray is then badly sized. I wonder if even a simple heuristic like taking the median of the last three FrameArray sizes would be more effective?
There was a problem hiding this comment.
Also: I think the comment on the function is misleading because the original capacity isn't kept. Some capacity is kept but it might not be all of it.
There was a problem hiding this comment.
So is the assumption that the
lowerSendQueuesends approximately the same number of items each time?This doesn't feel like it would be representative of real traffic patterns. and I'd be worried about one large or small batch negatively impacting perf because the next
FrameArrayis then badly sized. I wonder if even a simple heuristic like taking the median of the last threeFrameArraysizes would be more effective?
For larger transfers, yes, the send queue should be capped at 40 packets and if the congestion window is opened wide enough then we will fill this each time we send. There can be error here, yes, but for larger transfers this methodology is effective, as seen in the data above.
Just to make sure I am not regressing performance for highly concurrent, multi-stream, traffic patterns I ran our QUICStreamLoad benchmark with this change and with top of tree, and the results are almost a wash. So for these traffic patterns we would not be regressing performance.
Top of tree:
1.25 G 49.2% 14.00 M specialized QUICConnection.buildSinglePacketForKeyState()
1.72 G 16.4% 1.01 M specialized QUICConnection.sendApplicationFrames()
4.00 M 100.0% - drainArray
With this change
1.22 G 49.0% 14.00 M specialized QUICConnection.buildSinglePacketForKeyState()
1.74 G 16.6% - specialized QUICConnection.sendApplicationFrames()
7.00 M 100.0% - drainArrayKeepingCapacity
There was a problem hiding this comment.
Also: I think the comment on the function is misleading because the original capacity isn't kept. Some capacity is kept but it might not be all of it.
Removed comment in e2963a9
This one is a double performance win, we save around 150 megacycles and we avoid a temporary allocation for FrameArray each time SwiftQUIC sends.
Today, each time SwiftQUIC sends frames it builds a temporary outboundFrameArray to add all of these frames in and then adds these frames to the lowerSendQueue.
We can just add these frames directly to the lowerSendQueue and take a CPU win too as long as we keep the capacity of the lowerSendQueue.
Top of tree:
With this change: