Skip to content

Make hashing cheaper on hot paths - #85

Open
glbrntt wants to merge 2 commits into
apple:mainfrom
glbrntt:hashing
Open

Make hashing cheaper on hot paths#85
glbrntt wants to merge 2 commits into
apple:mainfrom
glbrntt:hashing

Conversation

@glbrntt

@glbrntt glbrntt commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Motivation:

Hasher.init and Hasher.finalize show up heavily in QUIC benchmarks
(~4-5% of cycles for 1KB download for 100 concurrent streams). There are
three main sources for this in swift-network:

  1. The transport parameters map,
  2. The knownFlows map in QUICConnection, and
  3. the MultiplexedFlowIdentifier lookup.

The transport parameters stores at most 19 elements and is keyed by an
enum: this alone is just shy of ~1% of the cycles. The known flows
lookup is also around ~1%, while the multiplexed flow ID accounts for
the rest.

Modifications:

  • Back TransportParameters by an array of optionals instead of a
    dictionary: the transport parameters types are each assigned a fixed
    index in the array. This eliminates hashing there altogether at the
    cost of ever so slightly more memory.
  • Key the known flows map in QUICConnection by the raw value for the
    stream ID instead of the nominal type. Swift special cases the hashing
    of various types to elide the creation of Hasher.
  • Implement the '_rawHashValue' customisation point in
    MultiplexedFlowIdentifier: this also avoids creating a Hasher.
  • The hash computation of MultiplexedFlowIdentifier also uses two bits
    as a discriminator for the underlying case.

Result:

The first two optimisation increase throughput in 1KB download QUIC
benchmarks by ~1.9%. Adding the multiplexed flow identifier change in
raises that to ~4.9%.

Motivation:

Hasher.init and Hasher.finalize show up heavily in QUIC benchmarks
(~4-5% of cycles for 1KB download for 100 concurrent streams). There are
three main sources for this in swift-network:

1. The transport parameters map,
2. The knownFlows map in QUICConnection, and
3. the MultiplexedFlowIdentifier lookup.

The transport parameters stores at most 19 elements and is keyed by an
enum: this alone is just shy of ~1% of the cycles. The known flows
lookup is also around ~1%, while the multiplexed flow ID accounts for
the rest.

Modifications:

- Back TransportParameters by an array of optionals instead of a
  dictionary: the transport parameters types are each assigned a fixed
  index in the array. This eliminates hashing there altogether at the
  cost of ever so slightly more memory.
- Key the known flows map in QUICConnection by the raw value for the
  stream ID instead of the nominal type. Swift special cases the hashing
  of various types to elide the creation of Hasher.
- Implement the '_rawHashValue' customisation point in
  MultiplexedFlowIdentifier: this also avoids creating a Hasher.
- The hash computation of MultiplexedFlowIdentifier also uses two bits
  as a discriminator for the underlying case.

Result:

The first two optimisation increase throughput in 1KB download QUIC
benchmarks by ~1.9%. Adding the multiplexed flow identifier change in
raises that to ~4.9%.
Comment on lines +184 to +186
public func _rawHashValue(seed: Int) -> Int {
self.rawHashKey._rawHashValue(seed: seed)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to get @lorentey's opinion on the use of this particular API since it's underscored...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great use of _rawHashValue! Forwarding to UInt64's implementation still ensures strong hashing, and marking the case in the two lower bits helps reduce collisions in the original implementation. 👍

var pendingReassemblyDequeue = QUICStreamList.pendingReassemblyDequeueList()

private(set) var knownFlows = [QUICStreamID: MultiplexedFlowIdentifier]()
// The logical key choice is 'QUICStreamID', however, Swift special cases the hashing of

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glbrntt what if we just made QUICStreamID be RawRepresentable and just be a raw UInt64? That's all it stores anyway.

@glbrntt glbrntt Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into that: sadly RawRepresentable hits the slow path! The default implementation of _rawHashValue(seed:) is this:

public func _rawHashValue(seed: Int) -> Int {
  var hasher = Hasher(_seed: seed)
  hasher.combine(self)
  return hasher._finalize()
}

So for RawRepresentable to get the fast path of not creating the Hasher it would have to forgo allowing combine(_:) to be customized.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to consider typealias QUICStreamID = UInt64 and then deal with the methods as extensions to UInt64 to avoid more problems like this?

@glbrntt glbrntt added the 🔨 semver/patch No public API change. label Aug 14, 2026
// The logical key choice is 'QUICStreamID', however, Swift special cases the hashing of
// various primitives (by avoiding the construction of a Hasher altogether). The result is
// that hashing the raw value is significantly cheaper which adds up on hot paths.
private(set) var knownFlows = [UInt64: MultiplexedFlowIdentifier]()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thank you for uncovering this @glbrntt !

@_spi(Essentials) @_spi(ProtocolProvider) @testable import SwiftNetwork

@available(Network 0.1.0, *)
final class SwiftNetworkTransportParametersTests: XCTestCase {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this test be part of the QUICTests instead?

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

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants