Skip to content

Commit 342e955

Browse files
committed
Tweak alignment and memory order in the ProbabilisticScorer
During routing, the majority of our time is spent in the scorer. Given the scorer isn't actually doing all that much computation, this means we're quite sensitive to memory latency. Thus, the cache lines our data sits on is incredibly important. Here, we manually lay out the `ChannelLiquidity` and `HistoricalLiquidityTracker` structs to ensure that we can do the non-historical scoring and skip historical scoring for channels with insufficient data by just looking at the same cache line the channel's SCID is on. Sadly, to do the full historical scoring we need to load a second 128-byte cache line pair, but we have some time to get there. We might consider issuing a preload instruction in the future. This improves performance a few percent.
1 parent f38d316 commit 342e955

File tree

1 file changed

+39
-5
lines changed

1 file changed

+39
-5
lines changed

lightning/src/routing/scoring.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ impl ProbabilisticScoringDecayParameters {
783783
/// Direction is defined in terms of [`NodeId`] partial ordering, where the source node is the
784784
/// first node in the ordering of the channel's counterparties. Thus, swapping the two liquidity
785785
/// offset fields gives the opposite direction.
786+
#[repr(C)] // Force the fields in memory to be in the order we specify
786787
struct ChannelLiquidity {
787788
/// Lower channel liquidity bound in terms of an offset from zero.
788789
min_liquidity_offset_msat: u64,
@@ -800,6 +801,16 @@ struct ChannelLiquidity {
800801
offset_history_last_updated: Duration,
801802
}
802803

804+
// Check that the liquidity HashMap's entries sit on round cache lines.
805+
//
806+
// Specifically, the first cache line will have the key, the liquidity offsets, and the total
807+
// points tracked in the historical tracker.
808+
//
809+
// The next two cache lines will have the historical points, which we only access last during
810+
// scoring, followed by the last_updated `Duration`s (which we do not need during scoring).
811+
const _LIQUIDITY_MAP_SIZING_CHECK: usize = 192 - ::core::mem::size_of::<(u64, ChannelLiquidity)>();
812+
const _LIQUIDITY_MAP_SIZING_CHECK_2: usize = ::core::mem::size_of::<(u64, ChannelLiquidity)>() - 192;
813+
803814
/// A snapshot of [`ChannelLiquidity`] in one direction assuming a certain channel capacity.
804815
struct DirectedChannelLiquidity<L: Deref<Target = u64>, HT: Deref<Target = HistoricalLiquidityTracker>, T: Deref<Target = Duration>> {
805816
min_liquidity_offset_msat: L,
@@ -1490,10 +1501,24 @@ mod bucketed_history {
14901501
// between the 12,000th sat and 24,000th sat, while only needing to store and operate on 32
14911502
// buckets in total.
14921503

1493-
const BUCKET_START_POS: [u16; 33] = [
1494-
0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 3072, 4096, 6144, 8192, 10240, 12288,
1495-
13312, 14336, 15360, 15872, 16128, 16256, 16320, 16352, 16368, 16376, 16380, 16382, 16383, 16384,
1496-
];
1504+
// By default u16s may not be cache-aligned, but we'd rather not have to read a third cache
1505+
// line just to access it
1506+
#[repr(align(128))]
1507+
struct BucketStartPos([u16; 33]);
1508+
impl BucketStartPos {
1509+
const fn new() -> Self {
1510+
Self([
1511+
0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 3072, 4096, 6144, 8192, 10240, 12288,
1512+
13312, 14336, 15360, 15872, 16128, 16256, 16320, 16352, 16368, 16376, 16380, 16382, 16383, 16384,
1513+
])
1514+
}
1515+
}
1516+
impl core::ops::Index<usize> for BucketStartPos {
1517+
type Output = u16;
1518+
#[inline(always)]
1519+
fn index(&self, index: usize) -> &u16 { &self.0[index] }
1520+
}
1521+
const BUCKET_START_POS: BucketStartPos = BucketStartPos::new();
14971522

14981523
const LEGACY_TO_BUCKET_RANGE: [(u8, u8); 8] = [
14991524
(0, 12), (12, 14), (14, 15), (15, 16), (16, 17), (17, 18), (18, 20), (20, 32)
@@ -1631,10 +1656,19 @@ mod bucketed_history {
16311656
impl_writeable_tlv_based!(LegacyHistoricalBucketRangeTracker, { (0, buckets, required) });
16321657

16331658
#[derive(Clone, Copy)]
1659+
#[repr(C)] // Force the fields in memory to be in the order we specify.
16341660
pub(super) struct HistoricalLiquidityTracker {
1661+
// This struct sits inside a `(u64, ChannelLiquidity)` in memory, and we first read the
1662+
// liquidity offsets in `ChannelLiquidity` when calculating the non-historical score. This
1663+
// means that the first handful of bytes of this struct will already be sitting in cache by
1664+
// the time we go to look at them.
1665+
//
1666+
// Because the first thing we do is check if `total_valid_points` is sufficient to consider
1667+
// the data here at all, and can return early if it is not, we want this to go first to
1668+
// avoid hitting a second cache line load entirely in that case.
1669+
total_valid_points_tracked: u64,
16351670
min_liquidity_offset_history: HistoricalBucketRangeTracker,
16361671
max_liquidity_offset_history: HistoricalBucketRangeTracker,
1637-
total_valid_points_tracked: u64,
16381672
}
16391673

16401674
impl HistoricalLiquidityTracker {

0 commit comments

Comments
 (0)