Skip to content

Commit 47ec7c6

Browse files
committed
OPTIM: vars: use a cebtree instead of a list for variable names
Configs involving many variables can start to eat a lot of CPU in name lookups. The reason is that the names themselves are dynamic in that they are relative to dynamic objects (sessions, streams, etc), so there's no fixed index for example. The current implementation relies on a standard linked list, and in order to speed up lookups and avoid comparing strings, only a 64-bit hash of the variable's name is stored and compared everywhere. But with just 100 variables and 1000 accesses in a config, it's clearly visible that variable name lookup can reach 56% CPU with a config generated this way: for i in {0..100}; do printf "\thttp-request set-var(txn.var%04d) int(%d)" $i $i; for j in {1..10}; do [ $i -lt $j ] || printf ",add(txn.var%04d)" $((i-j)); done; echo; done The performance and a 4-core skylake 4.4 GHz reaches 85k RPS with a perf profile showing: Samples: 170K of event 'cycles', Event count (approx.): 142378815419 Overhead Shared Object Symbol 56.39% haproxy [.] var_to_smp 6.65% haproxy [.] var_set.part.0 5.76% haproxy [.] sample_process_cnv 3.23% haproxy [.] sample_conv_var2smp 2.88% haproxy [.] sample_conv_arith_add 2.33% haproxy [.] __pool_alloc 2.19% haproxy [.] action_store 2.13% haproxy [.] vars_get_by_desc 1.87% haproxy [.] smp_dup [above, var_to_smp() calls var_get() under the read lock]. By switching to a binary tree, the cost is significantly lower, the performance reaches 117k RPS (+37%) with this profile: Samples: 170K of event 'cycles', Event count (approx.): 142323631229 Overhead Shared Object Symbol 40.22% haproxy [.] cebu64_lookup 7.12% haproxy [.] sample_process_cnv 6.15% haproxy [.] var_to_smp 4.75% haproxy [.] cebu64_insert 3.79% haproxy [.] sample_conv_var2smp 3.40% haproxy [.] cebu64_delete 3.10% haproxy [.] sample_conv_arith_add 2.36% haproxy [.] action_store 2.32% haproxy [.] __pool_alloc 2.08% haproxy [.] vars_get_by_desc 1.96% haproxy [.] smp_dup 1.75% haproxy [.] var_set.part.0 1.74% haproxy [.] cebu64_first 1.07% [kernel] [k] aq_hw_read_reg 1.03% haproxy [.] pool_put_to_cache 1.00% haproxy [.] sample_process The performance lowers a bit earlier than with the list however. What can be seen is that the performance maintains a plateau till 25 vars, starts degrading a little bit for the tree while it remains stable till 28 vars for the list. Then both cross at 42 vars and the list continues to degrade doing a hyperbole while the tree resists better. The biggest loss is at around 32 variables where the list stays 10% higher. Regardless, given the extremely narrow band where the list is better, it looks relevant to switch to this in order to preserve the almost linear performance of large setups. For example at 1000 variables and 10k lookups, the tree is 18 times faster than the list. In addition this reduces the size of the struct vars by 8 bytes since there's a single pointer, though it could make sense to re-invest them into a secondary head for example.
1 parent a0205f9 commit 47ec7c6

File tree

3 files changed

+29
-19
lines changed

3 files changed

+29
-19
lines changed

include/haproxy/vars-t.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
#include <haproxy/sample_data-t.h>
2626
#include <haproxy/thread-t.h>
27+
#include <import/cebtree.h>
2728

2829
/* flags used when setting/clearing variables */
2930
#define VF_CREATEONLY 0x00000001 // do nothing if the variable already exists
@@ -48,7 +49,7 @@ enum vars_scope {
4849
};
4950

5051
struct vars {
51-
struct list head;
52+
struct ceb_node *name_root;
5253
enum vars_scope scope;
5354
unsigned int size;
5455
__decl_thread(HA_RWLOCK_T rwlock);
@@ -64,8 +65,8 @@ struct var_desc {
6465
};
6566

6667
struct var {
67-
struct list l; /* Used for chaining vars. */
68-
uint64_t name_hash; /* XXH3() of the variable's name */
68+
struct ceb_node node; /* Used for chaining vars. */
69+
uint64_t name_hash; /* XXH3() of the variable's name, must be just after node */
6970
uint flags; // VF_*
7071
/* 32-bit hole here */
7172
struct sample_data data; /* data storage. */

include/haproxy/vars.h

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
#ifndef _HAPROXY_VARS_H
2323
#define _HAPROXY_VARS_H
2424

25+
#include <import/cebu64_tree.h>
26+
2527
#include <haproxy/api-t.h>
2628
#include <haproxy/session-t.h>
2729
#include <haproxy/stream-t.h>
@@ -34,7 +36,7 @@ struct arg;
3436

3537
void vars_init_head(struct vars *vars, enum vars_scope scope);
3638
void var_accounting_diff(struct vars *vars, struct session *sess, struct stream *strm, int size);
37-
unsigned int var_clear(struct var *var, int force);
39+
unsigned int var_clear(struct vars *vars, struct var *var, int force);
3840
void vars_prune_per_sess(struct vars *vars);
3941
int var_set(const struct var_desc *desc, struct sample *smp, uint flags);
4042
int var_unset(const struct var_desc *desc, struct sample *smp);
@@ -78,11 +80,13 @@ static inline void vars_rdunlock(struct vars *vars)
7880
*/
7981
static inline void vars_prune(struct vars *vars, struct session *sess, struct stream *strm)
8082
{
81-
struct var *var, *tmp;
83+
struct ceb_node *node;
84+
struct var *var;
8285
unsigned int size = 0;
8386

84-
list_for_each_entry_safe(var, tmp, &vars->head, l) {
85-
size += var_clear(var, 1);
87+
while ((node = cebu64_first(&vars->name_root))) {
88+
var = container_of(node, struct var, node);
89+
size += var_clear(vars, var, 1);
8690
}
8791

8892
if (!size)

src/vars.c

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include <haproxy/vars.h>
2121
#include <haproxy/xxhash.h>
2222

23+
#include <import/cebu64_tree.h>
24+
2325

2426
/* This contains a pool of struct vars */
2527
DECLARE_STATIC_POOL(var_pool, "vars", sizeof(struct var));
@@ -172,7 +174,7 @@ static int var_accounting_add(struct vars *vars, struct session *sess, struct st
172174
* using. If the variable is marked "VF_PERMANENT", the sample_data is only
173175
* reset to SMP_T_ANY unless <force> is non nul. Returns the freed size.
174176
*/
175-
unsigned int var_clear(struct var *var, int force)
177+
unsigned int var_clear(struct vars *vars, struct var *var, int force)
176178
{
177179
unsigned int size = 0;
178180

@@ -188,7 +190,7 @@ unsigned int var_clear(struct var *var, int force)
188190
var->data.type = SMP_T_ANY;
189191

190192
if (!(var->flags & VF_PERMANENT) || force) {
191-
LIST_DELETE(&var->l);
193+
cebu64_delete(&vars->name_root, &var->node);
192194
pool_free(var_pool, var);
193195
size += sizeof(struct var);
194196
}
@@ -200,11 +202,13 @@ unsigned int var_clear(struct var *var, int force)
200202
*/
201203
void vars_prune_per_sess(struct vars *vars)
202204
{
203-
struct var *var, *tmp;
205+
struct ceb_node *node;
206+
struct var *var;
204207
unsigned int size = 0;
205208

206-
list_for_each_entry_safe(var, tmp, &vars->head, l) {
207-
size += var_clear(var, 1);
209+
while ((node = cebu64_first(&vars->name_root))) {
210+
var = container_of(node, struct var, node);
211+
size += var_clear(vars, var, 1);
208212
}
209213

210214
if (!size)
@@ -219,7 +223,7 @@ void vars_prune_per_sess(struct vars *vars)
219223
/* This function initializes a variables list head */
220224
void vars_init_head(struct vars *vars, enum vars_scope scope)
221225
{
222-
LIST_INIT(&vars->head);
226+
vars->name_root = NULL;
223227
vars->scope = scope;
224228
vars->size = 0;
225229
HA_RWLOCK_INIT(&vars->rwlock);
@@ -327,11 +331,12 @@ static int vars_fill_desc(const char *name, int len, struct var_desc *desc, char
327331
*/
328332
static struct var *var_get(struct vars *vars, uint64_t name_hash)
329333
{
330-
struct var *var;
334+
struct ceb_node *node;
335+
336+
node = cebu64_lookup(&vars->name_root, name_hash);
337+
if (node)
338+
return container_of(node, struct var, node);
331339

332-
list_for_each_entry(var, &vars->head, l)
333-
if (var->name_hash == name_hash)
334-
return var;
335340
return NULL;
336341
}
337342

@@ -428,10 +433,10 @@ int var_set(const struct var_desc *desc, struct sample *smp, uint flags)
428433
var = pool_alloc(var_pool);
429434
if (!var)
430435
goto unlock;
431-
LIST_APPEND(&vars->head, &var->l);
432436
var->name_hash = desc->name_hash;
433437
var->flags = flags & VF_PERMANENT;
434438
var->data.type = SMP_T_ANY;
439+
cebu64_insert(&vars->name_root, &var->node);
435440
}
436441

437442
/* A variable of type SMP_T_ANY is considered as unset (either created
@@ -561,7 +566,7 @@ int var_unset(const struct var_desc *desc, struct sample *smp)
561566
vars_wrlock(vars);
562567
var = var_get(vars, desc->name_hash);
563568
if (var) {
564-
size = var_clear(var, 0);
569+
size = var_clear(vars, var, 0);
565570
var_accounting_diff(vars, smp->sess, smp->strm, -size);
566571
}
567572
vars_wrunlock(vars);

0 commit comments

Comments
 (0)