Skip to content

Commit 558b7a5

Browse files
committed
mem: HG_MALLOC - large tier: size-bucketed free lists instead of one first-fit walk
The large tier kept every free fragment on one unsorted list and walked it first-fit under the arena lock. With a million records loaded the lock histograms put that at 1.5 ms per large allocation on average (max 1.7 ms, 11 stalls over a millisecond on one sweep) - after the two-phase commit the single largest holder of the lock left. Free fragments now live in buckets by floor(log2(size)) with a bitmap of the non-empty buckets: a request scans only its own bucket and otherwise takes the head of the first non-empty bucket above it, where every fragment fits by construction. Chunks are doubly linked so an emptied chunk unlinks in O(1) instead of a walk of the chunk list. README 12.7: the receive-queue note now says plainly that the socket queue is the only buffer (one recvfrom() per datagram, no internal queue), that maxbuffer is what sizes it and net.core.rmem_max what bounds it, and that both have to be raised together.
1 parent dd92d81 commit 558b7a5

3 files changed

Lines changed: 88 additions & 30 deletions

File tree

‎mem/README.hg_malloc_v3.md‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -852,10 +852,26 @@ A worker parked for tens of milliseconds cannot drain its UDP socket;
852852
with the default 256 KB receive buffer a burst of ~1,500 INVITE-sized
853853
datagrams overflows it and the kernel drops (`UdpRcvbufErrors`) — the
854854
lost-request counts in every elastic run above came from exactly that.
855-
Raise the core `maxbuffer` (the `SO_RCVBUF` applied to SIP sockets) on
856-
nodes with large arenas, e.g. `maxbuffer = 4194304`, so a stall the
857-
allocator has not yet removed becomes latency rather than loss
858-
(`net.core.rmem_max` must allow it).
855+
The socket queue is the only buffer between the NIC and the workers
856+
(OpenSIPS reads one datagram at a time with `recvfrom()`; there is no
857+
second, internal queue), and its size is what OpenSIPS asks for at
858+
startup — the core parameter `maxbuffer`, 256 KB by default — bounded by
859+
what the kernel allows, `net.core.rmem_max`. Both have to move, and
860+
neither is code:
861+
862+
```
863+
sysctl -w net.core.rmem_max=8388608 # the kernel's ceiling
864+
maxbuffer = 8388608 # opensips.cfg: what the SIP sockets ask for
865+
```
866+
867+
Raising only the sysctl changes nothing (the sockets still ask for
868+
256 KB); raising only `maxbuffer` is clamped at the old ceiling. With
869+
both, a burst that a stall leaves unread parks in the queue and is
870+
drained afterwards, one datagram at a time — a stall the allocator has
871+
not yet removed becomes latency rather than loss. All UDP workers of a
872+
socket share that queue, so a single parked worker never drops anything;
873+
drops need every worker parked at once, which is what the two-phase
874+
commit ended.
859875

860876
### 12.5 The first soak, charted
861877

‎mem/hg_large.c‎

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
#ifdef HG_MALLOC
2424

25+
#include <string.h>
26+
2527
#include "hg_malloc.h"
2628
#include "hg_large.h"
2729
#include "hg_arena.h"
@@ -33,6 +35,7 @@
3335

3436
struct hg_large_chunk {
3537
struct hg_large_chunk *next;
38+
struct hg_large_chunk *prev; /* T21: O(1) unlink when a chunk empties */
3639
unsigned long size;
3740
/* the buddy block/run this chunk occupies, INCLUDING this header and
3841
* any rounding slack - what the grid actually lost, as opposed to
@@ -54,21 +57,39 @@ struct hg_large_chunk {
5457
#define HG_LFRAG_NEXT(f) \
5558
((struct hg_lfrag *)(void *)((char *)(f) + HG_LFRAG_HDR + (f)->size))
5659

60+
/* bucket = floor(log2(size)): every fragment in bucket k is >= 2^k, so a
61+
* request of need bytes fits ANY fragment in a bucket above its own and
62+
* only needs a scan inside its own bucket */
63+
static inline unsigned int lfrag_bucket(unsigned long size)
64+
{
65+
unsigned int b = size ? (unsigned int)(63 - __builtin_clzl(size)) : 0;
66+
67+
return b >= HG_LARGE_BUCKETS ? HG_LARGE_BUCKETS - 1 : b;
68+
}
69+
5770
static inline void lfrag_insert_free(struct hg_block *hb, struct hg_lfrag *frag)
5871
{
59-
frag->prev = &hb->large_free;
60-
frag->nxt_free = hb->large_free;
61-
if (hb->large_free)
62-
hb->large_free->prev = &frag->nxt_free;
63-
hb->large_free = frag;
72+
unsigned int b = lfrag_bucket(frag->size);
73+
74+
frag->prev = &hb->large_free[b];
75+
frag->nxt_free = hb->large_free[b];
76+
if (hb->large_free[b])
77+
hb->large_free[b]->prev = &frag->nxt_free;
78+
hb->large_free[b] = frag;
79+
hb->large_free_map |= 1UL << b;
6480
}
6581

66-
static inline void lfrag_remove_free(struct hg_lfrag *frag)
82+
/* call BEFORE changing frag->size: the bucket is derived from it */
83+
static inline void lfrag_remove_free(struct hg_block *hb, struct hg_lfrag *frag)
6784
{
85+
unsigned int b = lfrag_bucket(frag->size);
86+
6887
*(frag->prev) = frag->nxt_free;
6988
if (frag->nxt_free)
7089
frag->nxt_free->prev = frag->prev;
7190
frag->prev = NULL;
91+
if (!hb->large_free[b])
92+
hb->large_free_map &= ~(1UL << b);
7293
}
7394

7495
#ifdef DBG_MALLOC
@@ -98,15 +119,25 @@ void *hg_large_alloc(struct hg_block *hb, unsigned long size)
98119

99120
hg_lock_enter(hb, HG_LK_LARGE);
100121

101-
/* linear first-fit across ONE shared, unsorted free list (large
102-
* allocations are inherently rare and already slow-path - see the
103-
* hg_large.h file header for why this doesn't need f_malloc's
104-
* size-hashed buckets) */
105-
f = NULL;
106-
for (n = hb->large_free; n; n = n->nxt_free) {
107-
if (n->size >= need) {
108-
f = n;
109-
break;
122+
/* T21: first fit inside the request's own size bucket, else the head
123+
* of the first non-empty bucket above it (every fragment there fits
124+
* by construction) - the former single unsorted list cost 1.5 ms per
125+
* allocation under the arena lock with a million records loaded */
126+
{
127+
unsigned int b = lfrag_bucket(need);
128+
unsigned long higher;
129+
130+
f = NULL;
131+
for (n = hb->large_free[b]; n; n = n->nxt_free) {
132+
if (n->size >= need) {
133+
f = n;
134+
break;
135+
}
136+
}
137+
if (!f && b + 1 < HG_LARGE_BUCKETS) {
138+
higher = hb->large_free_map & ~((2UL << b) - 1);
139+
if (higher)
140+
f = hb->large_free[__builtin_ctzl(higher)];
110141
}
111142
}
112143

@@ -194,6 +225,9 @@ void *hg_large_alloc(struct hg_block *hb, unsigned long size)
194225
ch->last_frag->prev = NULL;
195226

196227
ch->next = hb->large_chunks;
228+
ch->prev = NULL;
229+
if (ch->next)
230+
ch->next->prev = ch;
197231
hb->large_chunks = ch;
198232
ch->backing = backing;
199233
ch->ord = chunk_ord;
@@ -218,7 +252,7 @@ void *hg_large_alloc(struct hg_block *hb, unsigned long size)
218252
/* f->size is guaranteed >= need: the chunk was sized for it */
219253
}
220254

221-
lfrag_remove_free(f);
255+
lfrag_remove_free(hb, f);
222256

223257
/* split off the remainder if it's big enough to be worth keeping as
224258
* its own free frag (mirrors f_malloc's fm_split_frag threshold) */
@@ -283,15 +317,15 @@ void hg_large_free(struct hg_block *hb, struct hg_lfrag *frag)
283317
* for a chunk's sentinel, so this naturally stops at the boundary */
284318
neigh = HG_LFRAG_NEXT(frag);
285319
if (neigh->prev) {
286-
lfrag_remove_free(neigh);
320+
lfrag_remove_free(hb, neigh);
287321
frag->size += HG_LFRAG_HDR + neigh->size;
288322
HG_LFRAG_NEXT(neigh)->pf = frag;
289323
}
290324

291325
/* backward coalesce */
292326
neigh = frag->pf;
293327
if (neigh && neigh->prev) {
294-
lfrag_remove_free(neigh);
328+
lfrag_remove_free(hb, neigh);
295329
neigh->size += HG_LFRAG_HDR + frag->size;
296330
HG_LFRAG_NEXT(frag)->pf = neigh;
297331
frag = neigh;
@@ -319,16 +353,17 @@ void hg_large_free(struct hg_block *hb, struct hg_lfrag *frag)
319353
((char *)frag - sizeof(struct hg_large_chunk));
320354

321355
if (ch->first_frag == frag) {
322-
struct hg_large_chunk **pp;
323356
void *base = ch;
324357
unsigned long backing = ch->backing;
325358
int ord = ch->ord;
326359

327-
for (pp = &hb->large_chunks; *pp; pp = &(*pp)->next)
328-
if (*pp == ch) {
329-
*pp = ch->next;
330-
break;
331-
}
360+
/* T21: O(1) unlink */
361+
if (ch->prev)
362+
ch->prev->next = ch->next;
363+
else
364+
hb->large_chunks = ch->next;
365+
if (ch->next)
366+
ch->next->prev = ch->prev;
332367

333368
hb->large_backing -= backing;
334369
hb->real_used -= backing;
@@ -384,7 +419,8 @@ void hg_large_destroy(struct hg_block *hb)
384419
* so there is still nothing to release one chunk at a time - only the
385420
* bookkeeping to drop. */
386421
hb->large_chunks = NULL;
387-
hb->large_free = NULL;
422+
memset(hb->large_free, 0, sizeof hb->large_free);
423+
hb->large_free_map = 0;
388424
hb->large_backing = 0;
389425
hb->large_live = 0;
390426
}

‎mem/hg_malloc.h‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,13 @@ struct hg_block {
432432
* list across all of them; hb->lock guards all of it (slow path
433433
* already, no separate lock needed) */
434434
struct hg_large_chunk *large_chunks;
435-
struct hg_lfrag *large_free;
435+
/* T21: free fragments bucketed by floor(log2(size)) with a bitmap of
436+
* the non-empty buckets - a fit is found in O(bucket) instead of a
437+
* first-fit walk of every free fragment, which the lock histograms put
438+
* at 1.5 ms mean per large allocation with a million records loaded */
439+
#define HG_LARGE_BUCKETS 40
440+
struct hg_lfrag *large_free[HG_LARGE_BUCKETS];
441+
unsigned long large_free_map;
436442
/*
437443
* Backing the large tier holds from the buddy grid, and how much of it
438444
* is handed out right now. Both are needed because the two answer

0 commit comments

Comments
 (0)