The direction of malloc?
Ondřej Bílka
neleai@seznam.cz
Tue Dec 17 22:00:00 GMT 2013
On Tue, Dec 17, 2013 at 01:59:57PM +0100, Torvald Riegel wrote:
> On Mon, 2013-12-16 at 22:23 +0100, OndÅej BÃlka wrote:
> > On Mon, Dec 16, 2013 at 05:59:00PM +0100, Torvald Riegel wrote:
> > > On Tue, 2013-12-10 at 22:05 +0100, OndÅej BÃlka wrote:
> > > > We have fastbins that sorta do this but with several problems.
> > > > 1. They are not really lockless, for malloc they need a lock, only
> > > > freeing will be when bug 15073 gets fixed.
> > >
> > > Note that *properly tuned* locks can be as fast as lock-less code in
> > > several cases. When you use a, say, stock PTHREAD_MUTEX_NORMAL lock,
> > > this won't be well-suited to short critical sections because it doesn't
> > > spin. PTHREAD_MUTEX_ADAPTIVE_NP spins, but the number of spinning
> > > iterations seems to be an arbitrary choice, and isn't very useful in my
> > > experiments so far. Also, there's no back-off whatsoever in the
> > > spinning.
> > >
> > As will said acquiring a lock is hot part in a single thread
> > applications.
>
> You said you wanted lock-less code. You didn't say you want
> nonconcurrent code. Thus there is concurrency, and you need to
> synchronize in some way. If it involves modifications, you need a CAS
> or other RMW op of some kind, typically, independent of whether you use
> a lock or not. Thus, the lock can have as many or fewer atomic RMW/CAS
> ops than other concurrent code. That's what I pointed out.
>
There are different degrees of synchronization that may be required. In
malloc sources of concurency are having many threads, free from
different thread and realloc from different thread.
A first one could be treated by a cache, for a free we could make a
synchronization overhead arbitrary low by waiting until we accumulate n
bytes for given arena and only then return these and I think that for
realloc best course of action is just copy data to our arena as I posted
earlier.
> > Please explain how spinning could improve performance in single thread
> > applications.
>
> You spoke about lockless code, so obviously concurrent code. My comment
> was thus referring to concurrent code. If you have a single-threaded
> program, then you can avoid synchronization, obviously (ignoring
> synchronization just for reentrancy...).
>
And we for malloc use a switch variable to avoid lock path and set it
when pthread_create is called? For reentancy a ordinary variable suffices.
> > >
> > > > Second problem is that fastbins are per-arena not per-thread which
> > > > forces us to use atomic operations. These are expensive (typicaly more than 50 cycles).
> > >
> > > Especially on x86, atomic operations that *hit in the cache* have become
> > > very fast compared to their costs in the past. I don't have current
> > > numbers, but I believe the 50 cycle number is too high; I vaguely
> > > remember 10-20.
> >
> > A simple benchmark could check a real cost. A problem is that while for core2
> > and i7 cost of CAS is around 20 cycles for bulldozer its still 50 cycles.
>
> Sure, this differs per architecture. But recent Intel CPUs *are*
> common, so it's not quite correct to say that the latency (or
> throughput, depending on the algorithm) of an atomic RMW/CAS op is
> really the primary problem.
>
> > Even with these a malloc+free pair contains 5 atomic instructions on
> > fastbin path which gives 100 cycle penalty (malloc: lock, get item from bin, unlock,
> > free: set bit that fastbins are in use, put item to bin)
>
> Maybe, but you still need to be careful when drawing conclusions from
> that because there are more performance effects in allocation than just
> the "cycle penalty" you mention (e.g., locality and other memory-system
> effects caused by differences in where data is placed).
>
Its not just that, pretty much every modern allocator uses some
per-thread cache so it looks like good idea, also oprofile shows lock
cmpxchg instruction as one of likely culprits. When I try a per-thread
cache that I posted earlier on a test that does just malloc and free it
nearly doubles performance.
There are several factors that come in play a first one is lack of
locking, second is getting expects correct, third is saving a call of
int_malloc. Then there are several cycles saved by omiting test for hook
and malloc_perturb.
Real implementation will be bit faster as dynamic tls slows this down a
bit.
Memory system effects are not a factor here, as allocation pattern is
identical (stack in both cases).
For benchmark use following commands:
$ gcc test.c -O3 -o test
$ gcc malloc_cache.c -O3 -fPIC -shared -mtls-dialect=gnu2 -lpthread -ldl -o malloc_cache.so
$ /usr/bin/time ./a.out
Command exited with non-zero status 32
0.29user 0.00system 0:00.29elapsed 99%CPU (0avgtext+0avgdata
512maxresident)k
0inputs+0outputs (0major+168minor)pagefaults 0swaps
$ /usr/bin/time ./a.out
Command exited with non-zero status 32
0.30user 0.00system 0:00.30elapsed 99%CPU (0avgtext+0avgdata
512maxresident)k
0inputs+0outputs (0major+168minor)pagefaults 0swaps
$ /usr/bin/time ./a.out
Command exited with non-zero status 32
0.30user 0.00system 0:00.31elapsed 99%CPU (0avgtext+0avgdata
512maxresident)k
0inputs+0outputs (0major+168minor)pagefaults 0swaps
$ LD_PRELOAD=./malloc_cache.so /usr/bin/time
./a.out
Command exited with non-zero status 32
0.19user 0.04system 0:00.23elapsed 98%CPU (0avgtext+0avgdata
117796maxresident)k
0inputs+0outputs (0major+29503minor)pagefaults 0swaps
$ LD_PRELOAD=./malloc_cache.so /usr/bin/time
./a.out
Command exited with non-zero status 32
0.17user 0.04system 0:00.22elapsed 99%CPU (0avgtext+0avgdata
117796maxresident)k
0inputs+0outputs (0major+29503minor)pagefaults 0swaps
$ LD_PRELOAD=./malloc_cache.so /usr/bin/time
./a.out
Command exited with non-zero status 32
0.12user 0.06system 0:00.19elapsed 99%CPU (0avgtext+0avgdata
117796maxresident)k
0inputs+0outputs (0major+29503minor)pagefaults 0swaps
-------------- next part --------------
#include <stdlib.h>
char *x[1024];
int main(){
int i;
for (i=0;i<10000000;i++)
{
free (x[i%1024]);
x[i%1024] = malloc (32 + (i % 64));
}
}
-------------- next part --------------
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#ifndef INTERNAL_SIZE_T
#define INTERNAL_SIZE_T size_t
#endif
/* The corresponding word size */
#define SIZE_SZ (sizeof(INTERNAL_SIZE_T))
#define CHUNKSIZE (2 * SIZE_SZ)
#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2
#define NON_MAIN_ARENA 0x4
#define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
typedef struct malloc_chunk* mchunkptr;
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))
static void *(*mallocp)(size_t);
static void *(*reallocp)(void *, size_t);
static void (*freep)(void *);
static pthread_key_t destructor_key;
static void free_thread_data (void *);
static void init_blocks()
{
pthread_key_create (&destructor_key, free_thread_data);
mallocp = dlsym (RTLD_NEXT, "malloc");
reallocp = dlsym (RTLD_NEXT, "realloc");
freep = dlsym (RTLD_NEXT, "free");
};
char temp[10000];
int tempno;
struct stack
{
struct stack *next;
};
struct thread_pool
{
struct stack *stack[17];
int recursed;
/* Frees from different stack or signal could be done by atomic addition to
this stack, that owner thread consumes. */
struct stack *to_free;
int zombie;
};
static __thread struct thread_pool pool;
static int inited;
static pthread_key_t key;
static void init_thread ()
{
if (!inited)
{
inited = 1;
init_blocks ();
inited = 2;
}
if (inited == 1)
abort();
pthread_setspecific(destructor_key, &key);
}
void *malloc_by_mmap (size_t size)
{
/* In separate patch. */
abort();
}
static void free_thread_data (void *data)
{
size_t idx;
for (idx = 1; idx <= 16; idx++)
{
struct stack *p = pool.stack[idx], *oldp;
while (p)
{
oldp = p;
p = p->next;
freep (oldp);
}
}
}
#define REC_FREE 1
#define REC_LOCK 2
static inline int as_lock ()
{
if (__builtin_expect (pool.recursed != REC_FREE, 0))
{
if (pool.recursed == 0)
{
init_thread ();
pool.recursed = REC_FREE;
}
else
return -1;
}
pool.recursed = REC_LOCK;
return 0;
}
static inline void as_unlock ()
{
pool.recursed = REC_FREE;
}
void *malloc (size_t size)
{
size_t idx = (size + CHUNKSIZE - 1) / (CHUNKSIZE);
if (__builtin_expect (inited == 1, 0))
{
char *ret = temp + tempno;
tempno += idx * CHUNKSIZE;
return ret;
}
if (__builtin_expect (as_lock (), 0))
return malloc_by_mmap (size);
if (__builtin_expect (idx <= 16, 1))
{
size_t idx = (size + CHUNKSIZE - 1) / CHUNKSIZE;
if (__builtin_expect (!pool.stack[idx], 0))
{
as_unlock ();
char *p = mallocp (size);
return p;
}
struct stack *ret = pool.stack[idx];
pool.stack[idx] = ret->next;
as_unlock ();
return (void *) ret;
}
else
{
as_unlock ();
char *p = mallocp (size);
return p;
}
}
void *calloc (size_t n, size_t el)
{
void *ret = malloc(n * el);
memset (ret, 0, n * el);
return ret;
}
void *realloc (void *old, size_t size)
{
return reallocp (old, size);
if (old == NULL)
return malloc (size);
mchunkptr p;
p = mem2chunk(old);
size_t oldsize = chunksize (p) - CHUNKSIZE;
size_t idx = (oldsize + CHUNKSIZE - 1) / CHUNKSIZE;
if (idx <= 16)
{
if (size > oldsize)
{
char *new = malloc (size);
memcpy (new, old, oldsize);
free (old);
return new;
}
else
return old;
}
reallocp (old, size);
}
void free (void *mem)
{
if (__builtin_expect (!mem, 0))
return;
mchunkptr p;
p = mem2chunk(mem);
size_t size = chunksize (p) - CHUNKSIZE;
size_t idx = (size + CHUNKSIZE - 1) / CHUNKSIZE;
if (__builtin_expect (idx <= 16, 1))
{
struct stack *ret = (struct stack *) mem;
if (as_lock ())
return;
ret->next = pool.stack[idx];
pool.stack[idx] = ret;
as_unlock ();
}
else
freep (mem);
}
More information about the Libc-alpha
mailing list