perf: Compare proper CanisterIds as u64 values#10664
Conversation
If a CanisterId actually represents a canister (the majority case; as opposed to a `SubnetId` masquerading as a canister), it is actually a `u64` value plus a couple of marker bytes underneath. Comparing such `CanisterIds` (both for equality but particularly for ordering) as `u64` values gives speed-ups between 1.25x and 2.8x (with the less common path only slowing down by 6-20%). We relatively rarely touch (or look up) the big `BTreeMap` of cold canisters anymore, but this will result in faster routing (and various canister call validations relying on the routing table). And faster `HashTree` creation. | Benchmark | `master` | this branch | speedup | |---|---:|---:|---| | `cmp` u64 | 3.49 ns | 1.71 ns | **2.04× faster** | | `binary_search` 1024 | 80.5 ns | 28.4 ns | **2.83× faster** | | `sort` 1024 | 5.84 µs | 3.79 µs | **1.54× faster** | | `eq` u64, differ | 2.22 ns | 1.77 ns | **1.25× faster** | | `eq` u64, equal | 2.22 ns | 1.76 ns | **1.26× faster** | | `linear_find` (eq scan) | 810 ns | 638 ns | **1.27× faster** | | `cmp` long/opaque | 3.46 ns | 3.68 ns | ~6% slower | | `eq` long, differ | 2.46 ns | 2.87 ns | ~17% slower | | `eq` long, equal | 2.26 ns | 2.70 ns | ~20% slower |
|
✅ No security or compliance issues detected. Reviewed everything up to 622ddfa. Security Overview
Detected Code Changes
|
|
✅ No security or compliance issues detected. Reviewed everything up to ffbabb8. Security Overview
Detected Code Changes
|
No changes in behavior, it simply drops the custom implementation of ic_registry_routing_table::canister_id_into_u64() with a call to newly added CanisterId::as_u64() (whose output is the identical).
…or proper, u64 CanisterIds), add exception for len() without is_empty(), drop explicit return statment.
| if self.is_u64 && other.is_u64 { | ||
| as_u64(self.id).cmp(&as_u64(other.id)) | ||
| } else { | ||
| self.id.cmp(&other.id) | ||
| } |
There was a problem hiding this comment.
One of the conditions that must hold when implementing both PartialOrd and PartialEq is a.eq(&b) <==> a.partial_cmp(&b) == Some(Equal), though it looks like if self.is_u64 != other.is_u64, couldn't we still have a.partial_cmp(&b) == Some(Equal) but a.eq(&b) == False?
There was a problem hiding this comment.
I don't think we could. a.is_u64 is equivalent to a.as_slice().len() == 10 && a.as_slice()[8] == 1 && a.as_slice()[9] == 1.
So self.is_u64 != other.is_u64 means that one of them has length 10 and ends in ..., 1, 1] and one either has a different length or it ends in different bytes. Regardless, cmp() would directly compare the slices and they could not possibly be equal.
If a
CanisterIdrepresents an actual canister (the majority case; as opposed to aSubnetIdmasquerading as a canister), it is actually a big-endianu64value plus a couple of marker bytes underneath. Comparing suchCanisterIds(both for equality but particularly for ordering) asu64values gives speed-ups between 1.25x and 2.8x (with the less common path only slowing down by 6-20%). And because theu64is encoded as big-endian, relative ordering remains the same.While the
CanisterIdstruct will grow by 1 byte (from 30 to 31 bytes) (i) that's all the increase (e.g. no extra 7 bytes due to memory alignment) and (ii) my guess is that in the vast majority of cases it actually takes up 32 bytes anyway (due to external alignment constraints).We relatively rarely touch (or look up) the big
BTreeMapof cold canisters anymore, but this will still be visible as faster message routing (and various canister call validations relying on the routing table). And fasterHashTreecreation.mastercmpu64binary_search1024sort1024equ64, differequ64, equallinear_find(eq scan)cmplong/opaqueeqlong, differeqlong, equalhashone u64 IDHashMaplookup, hitHashMaplookup, misshashone long/opaque ID