没有对齐的 malloc 或内存池
malloc or memory pool without alignment
我正在编写 C 代码,我有几个(数百万个)malloc,每个请求 20-30 字节的内存。
因此,GNU C Malloc 和 Jemalloc 的开销都达到了 40-50%。 DL Malloc 工作得更好,但仍有约 30% 的开销。
有没有办法在没有任何对齐/填充的情况下执行 malloc?我知道这会更慢,并且可能在某些 CPU 的 C 上需要 "reconstruct" 来自不同单词的数据,但我准备用速度换取内存使用。
我也可以使用内存池来代替 malloc,只要它可以在 free() 之后重用内存即可。
不一定慢。
如果块大小固定(或大小范围有限),或者您实际上按逻辑顺序分配和取消分配(FIFO / FILO),您通常可以通过处理 'pool'.[=14 来提高性能和内存管理=]
有一个 boost 库实现了它可能适合也可能不适合您的需要。
http://www.boost.org/doc/libs/1_57_0/libs/pool/doc/html/boost_pool/pool/interfaces.html
或者自己动手 - 分配大块内存并自己分割。
请注意,它可能不仅速度较慢,而且可能只是失败。
许多对齐的机器在被要求加载对齐地址之外时会 'go haywire' 例如,实际上加载将在 'correct' 值之前包含随机数据的向下舍入地址。
编译器绝对没有义务 'reconstruct' 值,我想说他们不这样做更常见。
因此,为了便于携带,您可能需要在使用前使用 memcpy()
将对齐数据移出对齐。这不一定有你认为的所有开销,因为一些编译器内联 memcpy()
.
因此池管理的分配通常可以更快(可能快得多)但 memcpy()
可能会更慢(尽管可能不会慢很多)。
这是秋千和环形交叉路口。
malloc()
等人。 C 标准要求为任何数据类型提供足够对齐的指针,因此要减少分配开销,您需要实现自己的分配器(或使用一些现有的分配器)。
一种可能性是为每个可能的分配大小设置一个池链。在每个池中,您可以使用位图来跟踪哪些项目已分配,哪些是空闲的。每个项目的开销刚刚超过一位,但您最终可能会拥有很多池链;这往往会使 free()
变慢,因为它必须搜索正确的池。
我认为更好的可能性是为小分配创建一个池链。在每个池中,小块形成一个链表,单个 unsigned char
跟踪长度和分配状态。这会产生未对齐的指针,但开销会超过一个字符。
例如:
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <stdio.h>
#define SMALL_POOL_SIZE 131072
#define SMALL_LIMIT ((UCHAR_MAX + 1U) / 2U)
struct small_pool {
struct small_pool *next;
unsigned int size;
unsigned int used;
unsigned char data[];
};
static struct small_pool *list = NULL;
在 data[]
成员中,对于该大小的空闲块,第一个字符是 1 到 SMALL_LIMIT-1,对于 1 个字符的已用块,第一个字符是 SMALL_LIMIT+1 或更大或者更多。零和 SMALL_LIMIT 表示错误。 space-激进的分配器可以实现为例如
void *small_alloc(const size_t size)
{
struct small_pool *pool;
if (size < 1 || size >= SMALL_LIMIT) {
errno = EINVAL;
return NULL;
}
pool = list;
while (pool != NULL) {
/* Unused space in the pool? */
if (pool->used + size < pool->size) {
unsigned char *const ptr = pool->data + pool->used;
/* Grab it. */
pool->used += size + 1U;
/* Create new slot. */
(*ptr) = size + SMALL_LIMIT;
/* Done. */
return ptr + 1U;
}
/* Check the free slots in the pool. */
{
unsigned char *const end = pool->data + pool->used;
unsigned char *ptr = pool->data;
unsigned char big_len = SMALL_LIMIT;
unsigned char *big_ptr = NULL;
while (ptr < end)
if (*ptr == 0U || *ptr == SMALL_LIMIT) {
/* Invalid pointer */
errno = EDOM;
return NULL;
} else
if (*ptr > SMALL_LIMIT) {
/* Used slot, skip */
ptr += (*ptr) - SMALL_LIMIT + 1U;
continue;
} else {
if (*ptr < size) {
/* Slot is too small, skip it */
ptr += (*ptr) + 1U;
continue;
} else
if (*ptr == size) {
/* Perfect slot; grab it. */
(*ptr) = size + SMALL_LIMIT;
return ptr + 1U;
} else
/* Remember smallest of the large enough slots */
if (*ptr < big_len) {
big_len = *ptr;
big_ptr = ptr;
}
ptr += (*ptr) + 1U;
}
if (big_ptr != NULL) {
(*big_ptr) = big_len + SMALL_LIMIT;
return big_ptr + 1;
}
}
/* Check the next pool. */
pool = pool->next;
}
/* Need a new pool. */
pool = malloc(SMALL_POOL_SIZE);
if (pool == NULL) {
errno = ENOMEM;
return NULL;
}
/* Initialize pool; use the initial slot for the new allocation. */
pool->used = size + 1;
pool->size = SMALL_POOL_SIZE - sizeof (struct small_pool);
pool->data[0] = size + SMALL_LIMIT;
/* Prepend this pool to the pool chain. */
pool->next = list;
list = pool;
/* Return the slot we used. */
return pool->data + 1;
}
它有一个简单的策略:如果池中有未使用的尾随 space,则使用它。否则,扫描池以查找未使用的插槽。如果有一个大小合适的插槽,就使用它;否则,使用最小的足够大的未使用插槽。
有很多改进的可能。例如,您可以将完整的池保存在单独的列表中,以避免扫描它们。将找到空闲插槽的池移动到池列表的开头也是一个好主意。
解除分配比较复杂。如果我们在分配中有相对较少的释放,并且不担心释放整个池,释放可以像
一样简单
int small_free(void *const item)
{
if (item == NULL)
return 0;
else {
struct small_pool *pool = list;
while (pool != NULL && !((unsigned char *)item > pool->data && (unsigned char *)item < pool->data + pool->used))
pool = pool->next;
if (pool != NULL) {
unsigned char *const ptr = (unsigned char *)item - 1;
if (*ptr > SMALL_LIMIT)
(*ptr) -= SMALL_LIMIT;
return 0;
}
return ENOENT;
}
}
假设您需要 return ENOENT
的功能,以防分配实际上不是一个小分配。如果验证要释放的指针是否有效很重要,比如调试,那么
int small_free(void *const item)
{
if (item == NULL)
return 0;
else {
struct small_pool *pool = list;
while (pool != NULL && !((unsigned char *)item > pool->data && (unsigned char *)item < pool->data + pool->used))
pool = pool->next;
if (pool != NULL) {
unsigned char *const end = pool->data + pool->used;
unsigned char *ptr = pool->data;
while (ptr < end)
if (*ptr == 0U || *ptr == SMALL_LIMIT)
return EDOM;
else
if (*ptr < SMALL_LIMIT) {
size_t len = (*ptr) + 1U;
/* Coalesce consecutive slots, if possible. */
while (len + ptr[len] < SMALL_LIMIT) {
(*ptr) = len + ptr[len];
len = (*ptr) + 1U;
}
ptr += len;
} else {
const size_t len = (*ptr) + 1U - SMALL_LIMIT;
/* Within the current slot.. probably should just
* compare item to ptr+1 instead. */
if ((unsigned char *)item > ptr && (unsigned char *)item < ptr + len) {
*ptr = len - 1U;
return 0;
}
ptr += len;
}
}
return ENOENT;
}
}
即使是后一个版本也不会 trim ->used
当池中的最后一个块被释放时,它也不会释放完全未使用的池。换句话说,上面的释放只是一个粗略的例子。
在速度方面,上述似乎至少比我机器上的 GLIBC malloc()
/free()
慢一个数量级。这是检查线性分配的简单测试 - 半随机重新分配模式:
/* Make sure this is prime wrt. 727 */
#define POINTERS 1000000
int main(void)
{
void **array;
size_t i;
fprintf(stderr, "Allocating an array of %lu pointers: ", (unsigned long)POINTERS);
fflush(stderr);
array = malloc((size_t)POINTERS * sizeof array[0]);
if (array == NULL) {
fprintf(stderr, "Failed.\n");
return EXIT_FAILURE;
}
fprintf(stderr, "Done.\n\n");
fprintf(stderr, "Allocating pointers in varying sizes.. ");
fflush(stderr);
for (i = 0; i < POINTERS; i++) {
const size_t n = 1 + ((i * 727) % (SMALL_LIMIT - 1));
if (!(array[i] = small_alloc(n))) {
if (errno == EDOM)
fprintf(stderr, "Failed at %lu; corrupted list.\n", (unsigned long)i + 1UL);
else
fprintf(stderr, "Failed at %lu: %s.\n", (unsigned long)i + 1UL, strerror(errno));
return EXIT_FAILURE;
}
}
fprintf(stderr, "Done.\n\n");
fprintf(stderr, "Deallocating pointers in a mixed order.. ");
fflush(stderr);
for (i = 0; i < POINTERS; i++) {
const size_t p = (i * 727) % POINTERS;
if (small_free(array[p])) {
if (errno == EDOM)
fprintf(stderr, "Failed at %lu: corrupted list.\n", (unsigned long)i + 1UL);
else
fprintf(stderr, "Failed at %lu: %s.\n", (unsigned long)i + 1UL, strerror(errno));
return EXIT_FAILURE;
}
}
fprintf(stderr, "Done.\n\n");
fprintf(stderr, "Deallocating the pointer array.. ");
fflush(stderr);
free(array);
fprintf(stderr, "Done.\n\n");
fflush(stderr);
return EXIT_SUCCESS;
}
在我看来,基于池的分配器的真正优势在于您可以一次释放整个池。考虑到这一点,也许您的工作负载可以在构建结构时使用专门的分配器,至少在最后使用压缩器阶段(能够调整指针)运行,如果足够数量,也可能在构建期间使用删除已经完成。该方法将允许您将临时需要的内存释放回 OS。 (如果没有压缩,大多数池可能至少有一个分配剩余,因此无法释放它。)
我认为这根本不是一个好的答案,但更好的答案需要更多细节,特别是关于存储的数据结构,以及实践中出现的 allocation/deallocation 模式。在没有这些的情况下,我希望这至少能提供一些关于如何进行的想法。
我正在编写 C 代码,我有几个(数百万个)malloc,每个请求 20-30 字节的内存。
因此,GNU C Malloc 和 Jemalloc 的开销都达到了 40-50%。 DL Malloc 工作得更好,但仍有约 30% 的开销。
有没有办法在没有任何对齐/填充的情况下执行 malloc?我知道这会更慢,并且可能在某些 CPU 的 C 上需要 "reconstruct" 来自不同单词的数据,但我准备用速度换取内存使用。
我也可以使用内存池来代替 malloc,只要它可以在 free() 之后重用内存即可。
不一定慢。 如果块大小固定(或大小范围有限),或者您实际上按逻辑顺序分配和取消分配(FIFO / FILO),您通常可以通过处理 'pool'.[=14 来提高性能和内存管理=]
有一个 boost 库实现了它可能适合也可能不适合您的需要。
http://www.boost.org/doc/libs/1_57_0/libs/pool/doc/html/boost_pool/pool/interfaces.html
或者自己动手 - 分配大块内存并自己分割。
请注意,它可能不仅速度较慢,而且可能只是失败。 许多对齐的机器在被要求加载对齐地址之外时会 'go haywire' 例如,实际上加载将在 'correct' 值之前包含随机数据的向下舍入地址。 编译器绝对没有义务 'reconstruct' 值,我想说他们不这样做更常见。
因此,为了便于携带,您可能需要在使用前使用 memcpy()
将对齐数据移出对齐。这不一定有你认为的所有开销,因为一些编译器内联 memcpy()
.
因此池管理的分配通常可以更快(可能快得多)但 memcpy()
可能会更慢(尽管可能不会慢很多)。
这是秋千和环形交叉路口。
malloc()
等人。 C 标准要求为任何数据类型提供足够对齐的指针,因此要减少分配开销,您需要实现自己的分配器(或使用一些现有的分配器)。
一种可能性是为每个可能的分配大小设置一个池链。在每个池中,您可以使用位图来跟踪哪些项目已分配,哪些是空闲的。每个项目的开销刚刚超过一位,但您最终可能会拥有很多池链;这往往会使 free()
变慢,因为它必须搜索正确的池。
我认为更好的可能性是为小分配创建一个池链。在每个池中,小块形成一个链表,单个 unsigned char
跟踪长度和分配状态。这会产生未对齐的指针,但开销会超过一个字符。
例如:
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <stdio.h>
#define SMALL_POOL_SIZE 131072
#define SMALL_LIMIT ((UCHAR_MAX + 1U) / 2U)
struct small_pool {
struct small_pool *next;
unsigned int size;
unsigned int used;
unsigned char data[];
};
static struct small_pool *list = NULL;
在 data[]
成员中,对于该大小的空闲块,第一个字符是 1 到 SMALL_LIMIT-1,对于 1 个字符的已用块,第一个字符是 SMALL_LIMIT+1 或更大或者更多。零和 SMALL_LIMIT 表示错误。 space-激进的分配器可以实现为例如
void *small_alloc(const size_t size)
{
struct small_pool *pool;
if (size < 1 || size >= SMALL_LIMIT) {
errno = EINVAL;
return NULL;
}
pool = list;
while (pool != NULL) {
/* Unused space in the pool? */
if (pool->used + size < pool->size) {
unsigned char *const ptr = pool->data + pool->used;
/* Grab it. */
pool->used += size + 1U;
/* Create new slot. */
(*ptr) = size + SMALL_LIMIT;
/* Done. */
return ptr + 1U;
}
/* Check the free slots in the pool. */
{
unsigned char *const end = pool->data + pool->used;
unsigned char *ptr = pool->data;
unsigned char big_len = SMALL_LIMIT;
unsigned char *big_ptr = NULL;
while (ptr < end)
if (*ptr == 0U || *ptr == SMALL_LIMIT) {
/* Invalid pointer */
errno = EDOM;
return NULL;
} else
if (*ptr > SMALL_LIMIT) {
/* Used slot, skip */
ptr += (*ptr) - SMALL_LIMIT + 1U;
continue;
} else {
if (*ptr < size) {
/* Slot is too small, skip it */
ptr += (*ptr) + 1U;
continue;
} else
if (*ptr == size) {
/* Perfect slot; grab it. */
(*ptr) = size + SMALL_LIMIT;
return ptr + 1U;
} else
/* Remember smallest of the large enough slots */
if (*ptr < big_len) {
big_len = *ptr;
big_ptr = ptr;
}
ptr += (*ptr) + 1U;
}
if (big_ptr != NULL) {
(*big_ptr) = big_len + SMALL_LIMIT;
return big_ptr + 1;
}
}
/* Check the next pool. */
pool = pool->next;
}
/* Need a new pool. */
pool = malloc(SMALL_POOL_SIZE);
if (pool == NULL) {
errno = ENOMEM;
return NULL;
}
/* Initialize pool; use the initial slot for the new allocation. */
pool->used = size + 1;
pool->size = SMALL_POOL_SIZE - sizeof (struct small_pool);
pool->data[0] = size + SMALL_LIMIT;
/* Prepend this pool to the pool chain. */
pool->next = list;
list = pool;
/* Return the slot we used. */
return pool->data + 1;
}
它有一个简单的策略:如果池中有未使用的尾随 space,则使用它。否则,扫描池以查找未使用的插槽。如果有一个大小合适的插槽,就使用它;否则,使用最小的足够大的未使用插槽。
有很多改进的可能。例如,您可以将完整的池保存在单独的列表中,以避免扫描它们。将找到空闲插槽的池移动到池列表的开头也是一个好主意。
解除分配比较复杂。如果我们在分配中有相对较少的释放,并且不担心释放整个池,释放可以像
一样简单int small_free(void *const item)
{
if (item == NULL)
return 0;
else {
struct small_pool *pool = list;
while (pool != NULL && !((unsigned char *)item > pool->data && (unsigned char *)item < pool->data + pool->used))
pool = pool->next;
if (pool != NULL) {
unsigned char *const ptr = (unsigned char *)item - 1;
if (*ptr > SMALL_LIMIT)
(*ptr) -= SMALL_LIMIT;
return 0;
}
return ENOENT;
}
}
假设您需要 return ENOENT
的功能,以防分配实际上不是一个小分配。如果验证要释放的指针是否有效很重要,比如调试,那么
int small_free(void *const item)
{
if (item == NULL)
return 0;
else {
struct small_pool *pool = list;
while (pool != NULL && !((unsigned char *)item > pool->data && (unsigned char *)item < pool->data + pool->used))
pool = pool->next;
if (pool != NULL) {
unsigned char *const end = pool->data + pool->used;
unsigned char *ptr = pool->data;
while (ptr < end)
if (*ptr == 0U || *ptr == SMALL_LIMIT)
return EDOM;
else
if (*ptr < SMALL_LIMIT) {
size_t len = (*ptr) + 1U;
/* Coalesce consecutive slots, if possible. */
while (len + ptr[len] < SMALL_LIMIT) {
(*ptr) = len + ptr[len];
len = (*ptr) + 1U;
}
ptr += len;
} else {
const size_t len = (*ptr) + 1U - SMALL_LIMIT;
/* Within the current slot.. probably should just
* compare item to ptr+1 instead. */
if ((unsigned char *)item > ptr && (unsigned char *)item < ptr + len) {
*ptr = len - 1U;
return 0;
}
ptr += len;
}
}
return ENOENT;
}
}
即使是后一个版本也不会 trim ->used
当池中的最后一个块被释放时,它也不会释放完全未使用的池。换句话说,上面的释放只是一个粗略的例子。
在速度方面,上述似乎至少比我机器上的 GLIBC malloc()
/free()
慢一个数量级。这是检查线性分配的简单测试 - 半随机重新分配模式:
/* Make sure this is prime wrt. 727 */
#define POINTERS 1000000
int main(void)
{
void **array;
size_t i;
fprintf(stderr, "Allocating an array of %lu pointers: ", (unsigned long)POINTERS);
fflush(stderr);
array = malloc((size_t)POINTERS * sizeof array[0]);
if (array == NULL) {
fprintf(stderr, "Failed.\n");
return EXIT_FAILURE;
}
fprintf(stderr, "Done.\n\n");
fprintf(stderr, "Allocating pointers in varying sizes.. ");
fflush(stderr);
for (i = 0; i < POINTERS; i++) {
const size_t n = 1 + ((i * 727) % (SMALL_LIMIT - 1));
if (!(array[i] = small_alloc(n))) {
if (errno == EDOM)
fprintf(stderr, "Failed at %lu; corrupted list.\n", (unsigned long)i + 1UL);
else
fprintf(stderr, "Failed at %lu: %s.\n", (unsigned long)i + 1UL, strerror(errno));
return EXIT_FAILURE;
}
}
fprintf(stderr, "Done.\n\n");
fprintf(stderr, "Deallocating pointers in a mixed order.. ");
fflush(stderr);
for (i = 0; i < POINTERS; i++) {
const size_t p = (i * 727) % POINTERS;
if (small_free(array[p])) {
if (errno == EDOM)
fprintf(stderr, "Failed at %lu: corrupted list.\n", (unsigned long)i + 1UL);
else
fprintf(stderr, "Failed at %lu: %s.\n", (unsigned long)i + 1UL, strerror(errno));
return EXIT_FAILURE;
}
}
fprintf(stderr, "Done.\n\n");
fprintf(stderr, "Deallocating the pointer array.. ");
fflush(stderr);
free(array);
fprintf(stderr, "Done.\n\n");
fflush(stderr);
return EXIT_SUCCESS;
}
在我看来,基于池的分配器的真正优势在于您可以一次释放整个池。考虑到这一点,也许您的工作负载可以在构建结构时使用专门的分配器,至少在最后使用压缩器阶段(能够调整指针)运行,如果足够数量,也可能在构建期间使用删除已经完成。该方法将允许您将临时需要的内存释放回 OS。 (如果没有压缩,大多数池可能至少有一个分配剩余,因此无法释放它。)
我认为这根本不是一个好的答案,但更好的答案需要更多细节,特别是关于存储的数据结构,以及实践中出现的 allocation/deallocation 模式。在没有这些的情况下,我希望这至少能提供一些关于如何进行的想法。