Skip to content

Commit ee79e66

Browse files
committed
mem: add HG_MALLOC, a hugepage-backed slab allocator, with CPU pinning
A fifth core allocator, selectable at runtime with -a/-s/-k HG_MALLOC alongside F_MALLOC, Q_MALLOC, HP_MALLOC and F_PARALLEL_MALLOC, plus the CPU pinning it needs to be worth having. Under load OpenSIPS spends a surprising share of its CPU inside the allocator - profiling a topology-hiding proxy put allocator self-time between 9.5% and 22.8% of total CPU depending on which of the existing four was in use. That is a large enough budget to be worth attacking directly rather than by shaving call paths elsewhere. The design takes two things the existing allocators do not. First, huge pages: the arena is acquired through a four-tier ladder - MAP_HUGETLB, then MADV_HUGEPAGE, then MADV_COLLAPSE, then plain 4K - and each tier is verified through /proc rather than assumed from a kernel version or a sysfs setting, degrading quietly and logging what it actually got. Second, and this turns out to matter more: allocation is served from fixed size classes off a per-process free stack, so the hot path takes no lock and touches no shared cache line. Measuring the tiers separately showed huge pages are only about a fifth of the win - tier 4, with huge pages entirely disabled, still beat F_MALLOC by 2.8x - so the allocator is worth using on machines with no hugetlb pool at all. Keeping free lists per process only pays while a process stays on one CPU, which OpenSIPS had no way to arrange: there was no affinity support in the core. So this adds it. pin_workers pins every worker round-robin; pin_udp_cpus / pin_tcp_cpus / pin_timer_cpus / pin_module_cpus confine one kind of process to a CPU list; and a UDP listener may name pin_cpus of its own, which is finer still and lets a public interface and a private one keep off each other's cores. Precedence runs listener, then process type, then unpinned. Every choice is intersected with the set the process is already allowed to run on, read back with sched_getaffinity(), so a cpuset or container can only ever narrow it, never widen it. The CPU is picked by occupancy over the processes running at that moment rather than from a process-table slot, which is what makes it behave under auto-scaling, where slots are freed and reused. One process is deliberately not pinned to a single CPU: TCP main is multithreaded - its IO pool defaults to one thread per online CPU - so a single-CPU pin would serialise every one of those threads onto one core. It is confined to the whole pin_tcp_cpus list instead, before its threads are created so they all inherit it, and it does not count as occupying a slot when the single-threaded TCP workers pick theirs. Measured on the topology-hiding path, HG_MALLOC costs 10-24% less CPU than F_MALLOC at matched load in containers, and 20-45% less on a dispatcher load balancer holding 20,000 calls in flight, with the gap widening as the load rises. It is not free: the arena is pre-faulted and mlock()ed at startup, so it is resident from the first second, and the size classes need roughly twice the shm F_MALLOC does for the same traffic. Both are documented, along with what running short looks like - loud in the log, but not fatal, which is the trap. CPU pinning is Linux-only and compiled out elsewhere with a warning rather than guessed at. The allocator itself is portable: ARM alignment and page-size assumptions are handled and asserted, and it has been cross-compiled for aarch64 and armv7, though not yet run on ARM hardware.
1 parent c05b576 commit ee79e66

32 files changed

Lines changed: 3615 additions & 14 deletions

‎Makefile.conf.template‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ DEFS+= -DQ_MALLOC #Quality assurance memory allocator with runtime safety checks
9797
DEFS+= -DHP_MALLOC #High performance allocator with fine-grained locking
9898
DEFS+= -DDBG_MALLOC #Include additional, debug-enabled allocator flavors
9999
DEFS+= -DF_PARALLEL_MALLOC #Fast memory allocator with paralel buckets
100+
DEFS+= -DHG_MALLOC #Hugepage-backed slab allocator with a lock-free fast path
101+
# HG_MALLOC reserves and pins its own arena, serves fixed size classes out of
102+
# it, and never hands memory back from one class to another - so it needs
103+
# roughly twice the -m that F_MALLOC needs for the same traffic, and says so
104+
# via carve_chunk errors when it does not have it. See mem/README.hg_malloc
100105
#DEFS+= -DQM_DBG_MALLOC_HIST=3 #Size of debug history for Q_MALLOC (default is 1)
101106
#DEFS+= -DNO_DEBUG #Compile out all debug messages
102107
#DEFS+= -DNO_LOG #Compile out all logging

‎Makefile.defs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,12 @@ endif
524524
ifeq (,$(findstring F_MALLOC, $(DEFS)))
525525
ifeq (,$(findstring Q_MALLOC, $(DEFS)))
526526
ifeq (,$(findstring HP_MALLOC, $(DEFS)))
527+
ifeq (,$(findstring HG_MALLOC, $(DEFS)))
527528
DEFS+= -DQ_MALLOC
528529
endif
529530
endif
530531
endif
532+
endif
531533

532534
ifneq (,$(MEM_STATS_HDR))
533535
EXPECTED_MD5=$(shell [ -f $(MEM_STATS_HDR) ] && tail -n 1 $(MEM_STATS_HDR) | awk '{print $$4}')
@@ -558,6 +560,10 @@ MKTAGS=ctags -R .
558560
# decently fast allocator with extra sanity checks + safety buffers
559561
# -DHP_MALLOC
560562
# high performance allocator optimized for shared memory multiprocessing
563+
# -DHG_MALLOC
564+
# hugepage-backed slab allocator; fixed size classes bump-carved from
565+
# its own huge-page (or best-effort degraded) reservation, no fallback
566+
# to another allocator on exhaustion - increase -m/-M instead
561567
# -DDBG_MALLOC
562568
# will cause each of the enabled allocators to also include a "debugging"
563569
# flavor of itself; by running a debugging version of an allocator, you

‎cfg.lex‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ RPM_MEM_FILE "restart_persistency_cache_file"
246246
RPM_MEM_SIZE "restart_persistency_size"
247247
MEMLOG "memlog"|"mem_log"
248248
MEMDUMP "memdump"|"mem_dump"
249+
PIN_WORKERS "pin_workers"|"cpu_pinning"
250+
PIN_UDP_CPUS "pin_udp_cpus"
251+
PIN_TCP_CPUS "pin_tcp_cpus"
252+
PIN_TIMER_CPUS "pin_timer_cpus"
253+
PIN_MODULE_CPUS "pin_module_cpus"
249254
SHM_MEMLOG_SIZE "shm_memlog_size"
250255
EXECMSGTHRESHOLD "execmsgthreshold"|"exec_msg_threshold"
251256
EXECDNSTHRESHOLD "execdnsthreshold"|"exec_dns_threshold"
@@ -318,6 +323,7 @@ TICK \'
318323
SLASH "/"
319324
AS {EAT_ABLE}("as"|"AS"){EAT_ABLE}
320325
USE_WORKERS {EAT_ABLE}("use_workers"|"USE_WORKERS"){EAT_ABLE}
326+
PIN_CPUS {EAT_ABLE}("pin_cpus"|"PIN_CPUS"){EAT_ABLE}
321327
SOCK_TOS {EAT_ABLE}("tos"|"TOS"){EAT_ABLE}
322328
USE_AUTO_SCALING_PROFILE {EAT_ABLE}("use_auto_scaling_profile"|"USE_AUTO_SCALING_PROFILE"){EAT_ABLE}
323329
SCALE_UP_TO {EAT_ABLE}("scale"|"SCALE"){EAT_ABLE}+("up"|"UP"){EAT_ABLE}+("to"|"TO"){EAT_ABLE}
@@ -480,6 +486,11 @@ SPACE [ ]
480486
<INITIAL>{RPM_MEM_SIZE} { count(); yylval.strval=yytext; return RPM_MEM_SIZE; }
481487
<INITIAL>{MEMLOG} { count(); yylval.strval=yytext; return MEMLOG; }
482488
<INITIAL>{MEMDUMP} { count(); yylval.strval=yytext; return MEMDUMP; }
489+
<INITIAL>{PIN_WORKERS} { count(); yylval.strval=yytext; return PIN_WORKERS; }
490+
<INITIAL>{PIN_UDP_CPUS} { count(); yylval.strval=yytext; return PIN_UDP_CPUS; }
491+
<INITIAL>{PIN_TCP_CPUS} { count(); yylval.strval=yytext; return PIN_TCP_CPUS; }
492+
<INITIAL>{PIN_TIMER_CPUS} { count(); yylval.strval=yytext; return PIN_TIMER_CPUS; }
493+
<INITIAL>{PIN_MODULE_CPUS} { count(); yylval.strval=yytext; return PIN_MODULE_CPUS; }
483494
<INITIAL>{SHM_MEMLOG_SIZE} { count(); yylval.strval=yytext; return SHM_MEMLOG_SIZE; }
484495
<INITIAL>{EXECMSGTHRESHOLD} { count(); yylval.strval=yytext; return EXECMSGTHRESHOLD; }
485496
<INITIAL>{EXECDNSTHRESHOLD} { count(); yylval.strval=yytext; return EXECDNSTHRESHOLD; }
@@ -601,6 +612,7 @@ SPACE [ ]
601612
<INITIAL>{COMMA} { count(); return COMMA; }
602613
<INITIAL>{SEMICOLON} { count(); return SEMICOLON; }
603614
<INITIAL>{USE_WORKERS} { count(); return USE_WORKERS; }
615+
<INITIAL>{PIN_CPUS} { count(); return PIN_CPUS; }
604616
<INITIAL>{SOCK_TOS} { count(); return SOCK_TOS; }
605617
<INITIAL>{USE_AUTO_SCALING_PROFILE} { count(); return USE_AUTO_SCALING_PROFILE; }
606618
<INITIAL>{COLON} { count(); return COLON; }

‎cfg.y‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ extern char *finame;
182182
struct listen_param {
183183
enum si_flags flags;
184184
int workers;
185+
char *pin_cpus;
185186
int tos;
186187
struct socket_id *socket;
187188
char *tag;
@@ -352,6 +353,11 @@ extern int cfg_parse_only_routes;
352353
%token RPM_MEM_SIZE
353354
%token MEMLOG
354355
%token MEMDUMP
356+
%token PIN_WORKERS
357+
%token PIN_UDP_CPUS
358+
%token PIN_TCP_CPUS
359+
%token PIN_TIMER_CPUS
360+
%token PIN_MODULE_CPUS
355361
%token SHM_MEMLOG_SIZE
356362
%token EXECMSGTHRESHOLD
357363
%token EXECDNSTHRESHOLD
@@ -459,6 +465,7 @@ extern int cfg_parse_only_routes;
459465
%token SLASH
460466
%token AS
461467
%token USE_WORKERS
468+
%token PIN_CPUS
462469
%token SOCK_TOS
463470
%token USE_AUTO_SCALING_PROFILE
464471
%token MAX
@@ -761,6 +768,9 @@ socket_def_param: ANYCAST { IFOR();
761768
| USE_WORKERS NUMBER { IFOR();
762769
p_tmp.workers=$2;
763770
}
771+
| PIN_CPUS STRING { IFOR();
772+
p_tmp.pin_cpus=$2;
773+
}
764774
| SOCK_TOS NUMBER { IFOR();
765775
p_tmp.tos=$2;
766776
}
@@ -1257,6 +1267,16 @@ assign_stm: LOGLEVEL EQUAL snumber { IFOR();
12571267
| SHM_MEMLOG_SIZE EQUAL error { yyerror("int value expected"); }
12581268
| MEMDUMP EQUAL snumber { IFOR(); memdump=$3; }
12591269
| MEMDUMP EQUAL error { yyerror("int value expected"); }
1270+
| PIN_WORKERS EQUAL snumber { IFOR(); pin_workers=$3; }
1271+
| PIN_WORKERS EQUAL error { yyerror("int value expected"); }
1272+
| PIN_UDP_CPUS EQUAL STRING { IFOR(); pin_udp_cpus=$3; pin_workers=1; }
1273+
| PIN_UDP_CPUS EQUAL error { yyerror("string value expected"); }
1274+
| PIN_TCP_CPUS EQUAL STRING { IFOR(); pin_tcp_cpus=$3; pin_workers=1; }
1275+
| PIN_TCP_CPUS EQUAL error { yyerror("string value expected"); }
1276+
| PIN_TIMER_CPUS EQUAL STRING { IFOR(); pin_timer_cpus=$3; pin_workers=1; }
1277+
| PIN_TIMER_CPUS EQUAL error { yyerror("string value expected"); }
1278+
| PIN_MODULE_CPUS EQUAL STRING { IFOR(); pin_module_cpus=$3; pin_workers=1; }
1279+
| PIN_MODULE_CPUS EQUAL error { yyerror("string value expected"); }
12601280
| EXECMSGTHRESHOLD EQUAL NUMBER { IFOR();execmsgthreshold=$3; }
12611281
| EXECMSGTHRESHOLD EQUAL error { yyerror("int value expected"); }
12621282
| EXECDNSTHRESHOLD EQUAL NUMBER { IFOR(); execdnsthreshold=$3; }
@@ -2793,6 +2813,7 @@ static void fill_socket_id(struct listen_param *param, struct socket_id *s)
27932813
while (s) {
27942814
s->flags |= param->flags;
27952815
s->workers = param->workers;
2816+
s->pin_cpus = param->pin_cpus;
27962817
s->tos = param->tos;
27972818
s->auto_scaling_profile = param->auto_scaling_profile;
27982819
s->tag = param->tag;

‎globals.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ int check_via = 0;
9595
/* debugging level for memory stats */
9696
int memlog = L_DBG + 11;
9797
int memdump = L_DBG + 10;
98+
/* pin each worker to one CPU (0 = off). See pt.c pin_worker_to_cpu(). */
99+
int pin_workers = 0;
100+
/* optional per-process-type CPU lists, e.g. "0-7". NULL = all allowed CPUs */
101+
char *pin_udp_cpus = NULL;
102+
char *pin_tcp_cpus = NULL;
103+
char *pin_timer_cpus = NULL;
104+
char *pin_module_cpus = NULL;
98105
unsigned int shm_memlog_size = 0;
99106
/* debugging in case msg processing takes. too long disabled by default */
100107
int execmsgthreshold = 0;

‎globals.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ extern int is_pre_daemon;
121121

122122
extern int memlog; /*!< debugging level for printing memory debugs */
123123
extern int memdump; /*!< debugging level for dumping memory status */
124+
extern int pin_workers; /*!< pin each worker process to a single CPU */
125+
extern char *pin_udp_cpus; /*!< CPU list for UDP workers */
126+
extern char *pin_tcp_cpus; /*!< CPU list for TCP workers */
127+
extern char *pin_timer_cpus; /*!< CPU list for timer processes */
128+
extern char *pin_module_cpus; /*!< CPU list for module processes */
124129
extern unsigned int shm_memlog_size;
125130
extern int execmsgthreshold; /*!< Maximum number of microseconds a SIP msg processing can last
126131
before triggering Warning log */

‎help_msg.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Options:\n\
9696
F_PARALLEL_MALLOC F_PARALLEL_MALLOC_DBG\n\
9797
Q_MALLOC Q_MALLOC_DBG\n\
9898
HP_MALLOC HP_MALLOC_DBG\n\
99+
HG_MALLOC HG_MALLOC_DBG\n\
99100
-k allocator The pkg memory allocator to use (overrides -a)\n\
100101
-s allocator The shared memory allocator to use (overrides -a)\n\
101102
-e allocator The restart-persistent memory allocator to use (overrides -a)\n\

‎ip_addr.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ struct socket_id {
154154
int proto;
155155
int port;
156156
int workers;
157+
char* pin_cpus;
157158
int tos;
158159
enum si_flags flags;
159160
struct socket_id* next;

0 commit comments

Comments
 (0)