当前位置: 首页 > 知识库问答 >
问题:

malloc或无对齐的内存池

贺运良
2023-03-14

我正在做C代码,我有几个(数百万)malloc,每个请求20-30字节的内存。

因此,GNU C Malloc和Jemalloc的开销都达到了40-50%。DL-Malloc工作得更好,但仍有约30%的开销。

有没有一种方法可以在没有任何对齐/填充的情况下执行malloc?我知道这会很慢,而且可能在某些CPU上需要从不同的单词“重建”数据,但我准备以速度换取内存使用量。

我也可以使用内存池来代替malloc,只要它在free()之后可以重用内存。

共有2个答案

花俊雄
2023-03-14
匿名用户

它不一定慢。如果块的大小是固定的(或大小范围有限),或者实际上是按逻辑顺序(FIFO/FILO)分配和未分配,则通常可以通过处理“池”来提高性能和内存管理。

有一个boost库实现了可能适合或不适合您的需求。

http://www.boost.org/doc/libs/1_57_0/libs/pool/doc/html/boost_pool/pool/interfaces.html

或者自己做——分配大块内存并自己瓜分。

请注意,它可能不仅速度较慢,而且可能会失败。许多对齐的机器在被要求加载未对齐的地址时会“失控”。例如,实际上加载的是向下舍入的地址,该地址将包含“正确”值之前的随机数据。编译器绝对没有义务“重建”值,我想说的是,他们不这样做比不这样做更常见。

因此,为了可移植性,您可能需要使用memcpy()在使用前将对齐数据移出对齐。这并不一定具有您认为某些编译器内联memcpy()的所有开销。

因此,池管理的分配通常可以更快(可能快得多),但memcpy()可能会更慢(尽管可能不会慢那么多)。

这是秋千和环岛。

姬歌者
2023-03-14

malloc()等人。C标准要求为任何数据类型提供充分对齐的指针,因此为了减少分配开销,您需要实现自己的分配器(或使用一些现有的分配器)。

一种可能是为每个可能的分配大小建立一个池链。在每个池中,您可以使用位图来跟踪哪些项已分配,哪些项可用。每个项目的开销略高于一位,但最终可能会有很多池链;这往往会使free()变慢,因为它必须搜索正确的池。

我认为,更好的可能性是为小分配创建一个池链。在每个池中,小块形成一个链表,单个无符号字符跟踪长度和分配状态。这会产生未对齐的指针,但开销略高于一个字符。

例如:

#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;

在数据[]成员中,对于该大小的空闲块,第一个字符是1到SMALL\u LIMIT-1,对于1个字符或更多的已用块,第一个字符是SMALL\u LIMIT 1或更大。零和小_限制表示错误。占用空间的分配器可以实现为

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;
}

它有一个简单的策略:如果池中有未使用的尾随空间,请使用它。否则,请扫描池以查找未使用的插槽。如果有一个大小合适的插槽,请使用它;否则,请使用最小且足够大的未使用插槽。

有许多改进是可能的。例如,您可以将完整的池保存在一个单独的列表中,以避免扫描它们。将空闲插槽所在的池移动到池列表的开头也是一个好主意。

解除分配更为复杂。如果我们在分配中有相对较少的释放,并且不担心释放整个池,那么释放可以简单到

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;    
    }
}

假设您需要函数返回enoint,以防分配实际上不是很小。如果验证要解除分配的指针是否有效很重要,例如用于调试,那么

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;    
    }
}

即使是后一个版本也不会修剪-

就速度而言,上述操作似乎至少比我的机器上的GLIBCmalloc()/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;
}

在我看来,基于池的分配器的真正优势在于你可以一次释放整个池。考虑到这一点,也许你的工作负载可以在构建结构时使用一个专门的分配器,压缩阶段(能够调整指针)至少在最后运行,如果已经完成了足够数量的删除,也可能在构建过程中运行。这种方法将允许您将临时需要的内存释放回操作系统。(如果没有压缩,大多数池可能至少还剩下一个分配,因此无法释放它。)

我认为这根本不是一个好的答案,但是一个更好的答案需要更多的细节,特别是关于存储的数据结构,以及在实践中发生的分配/释放模式。在没有这些的情况下,我希望这至少能给出一些关于如何进行的想法。

 类似资料:
  • 当目标指令集为x86/x64时,未对齐的内存读写不会导致错误的结果;而在Emscripten环境下,编译目标为asm.js与WebAssembly时,情况又各有不同。 info 这里“未对齐”的含义是:欲访问的内存地址不是欲访问的数据类型大小的整数倍。 4.2.1 asm.js C代码如下: //unaligned.cc struct ST { uint8_t c[4]; float f; }

  • 问题内容: 首先,我注意到当我分配内存与calloc时,内存占用量是不同的。我正在使用数GB的数据集。此数据可以是随机的。 我以为我可以分配大量的内存并读取其中的任何随机数据,然后将其强制转换为浮点数。但是,在进程查看器中查看内存占用量时,显然并没有要求使用内存(与calloc相比,我看到了较大的占用空间)。我运行了一个循环,将数据写入内存,然后看到内存占用量攀升。 我是否正确地说,直到初始化内存

  • 我不得不问:从逻辑上讲,为什么地址边界本身在什么上可分很重要?使用地址上的整数将一组内存分配给的调优有什么问题? 我知道指针算术是如何工作的,但我无法计算边界的重要性······

  • 问题内容: 每次从stdin读取字母“ u”时,此代码段将分配2Gb,并且在读取“ a”后将初始化所有分配的字符。 我在具有3Gb内存的linux虚拟机上运行此代码。在使用htop工具监视系统资源使用情况时,我已经意识到malloc操作不会反映在资源上。 例如,当我仅输入一次“ u”(即分配2GB的堆内存)时,我看不到htop中的内存使用量增加2GB。只有当我输入“ a”(即初始化)时,我才会看到

  • 本文向大家介绍C++中的内存对齐实例详解,包括了C++中的内存对齐实例详解的使用技巧和注意事项,需要的朋友参考一下 C++中的内存对齐实例详解 内存对齐          在我们的程序中,数据结构还有变量等等都需要占有内存,在很多系统中,它都要求内存分配的时候要对齐,这样做的好处就是可以提高访问内存的速度。 我们还是先来看一段简单的程序:            程序一       这段程序的功能很

  • 问题内容: 在linux系统中,pthreads库为我们提供了用于对齐缓存的功能(posix_memalign),以防止错误共享。要选择架构的特定NUMA节点,我们可以使用libnuma库。我想要的是同时需要两者的东西。我将某些线程绑定到某些处理器,并且我想为来自相应NUMA节点的每个线程分配本地数据结构,以减少线程的内存操作延迟。我怎样才能做到这一点? 问题答案: 如果您只是希望围绕NUMA分配