全页分配内存

6

我正在尝试通过一次性使用整个页面来优化程序的内存分配。

我通过这样获取页面大小:sysconf(_SC_PAGESIZE);,然后像这样计算一页能够容纳的元素总数:elements=pageSize/sizeof(Node);

我考虑当我实际调用malloc分配内存时,我会使用 malloc(elements*sizeof(Node));。似乎sizeof(Node)的乘法和除法将会互相抵消,但是由于整数除法,我不认为会这样。

这是一次性分配整页内存的最佳方法吗?

谢谢


4
请使用 mmap/VirtualAlloc... - nneonneo
2
顺便说一下,如果您的节点大小超过页面大小,那么您将不会喜欢您每页元素方程的答案。 - WhozCraig
1
你为什么想要精确地分配页面大小的内存?这样做并不高效。 - R.. GitHub STOP HELPING ICE
1
没有根本的理由期望精确的页面大小分配比其他大小高效,并且由于各种原因,它们很可能不太高效。如果 malloc 执行新的 mmap 来服务请求,则需要分配至少2页才能给调用者一个页面,因为没有相邻的书记信息,无法使 free 高效(O(1)时间)。该余额将不可用于应用程序并将被浪费。在实践中,malloc 将从堆中提供这样的小分配。 - R.. GitHub STOP HELPING ICE
1
在这种情况下,内存不会被浪费,但是malloc将会触及2个页面来更新簿记信息,即使您只使用一个页面,这可能会导致额外的页面错误,如果第二个页面尚未备份或已被交换出。无论如何,正如我所说,我看不到确切页面大小分配会更好的原因,而且有很多理由它可能会更糟。我会根据您的程序需求选择一边,而不是基于对硬件或库实现行为的错误假设。 - R.. GitHub STOP HELPING ICE
显示剩余6条评论
5个回答

8
malloc函数没有任何页面大小的概念。除非您分配的页面也与页面边界对齐,否则调用malloc将不能从中受益。只需要使用malloc分配所需数量的元素,不要担心微观优化,因为这几乎肯定不会给您带来任何好处。
是的,Linux内核经常这样做。原因如下:
  1. 您不希望分配大于页面大小的块,因为这会显着增加分配失败的风险。
  2. 内核分配基于每页,而不是像C库一样将大量内存分配一次,然后将其拆分成小组件。
如果您真的想分配一页内存,请使用sysconf(_SC_PAGESIZE)函数的结果作为大小参数。但几乎可以肯定您的分配跨越两个页面。

1
错误,它“保证”跨越两页甚至三页:malloc(3)必须为交给您的程序的空间添加一些簿记空间。 - vonbrand
是的,我故意忽略了开销。当然,这个部分可能位于4KB页面之外... - Mats Petersson
1
@vonbrand:一个malloc实现可以将元数据存储在其他地方。 - ysdx
2
@ysdx 可能会这样,所以他的评论有些过于强烈,但是绝大多数的 malloc 实现都会在返回的内存之前存储它们的开销,这样当需要时就可以非常快速地访问它们。 - jschultz410
多么糟糕的答案。有时候你想要页面对齐的内存,因为你想将该内存页设置为可执行。 - Enerccio

5
您的计算公式 elements=pageSize/sizeof(Node); 没有考虑到任何由malloc()返回的内存块/分块中添加的元数据。在许多情况下,malloc()将返回一个内存块,该内存块可能至少对齐于 min(sizeof(double),2 * sizeof(void *)) 边界(顺便说一句,32字节正在变得越来越常见...)。如果malloc()获得在页面上对齐的内存块,并添加了其块(带填充),并且您编写了整个页面大小的数据,则最后几个字节会偏离第一页:因此您最终使用了2个页面。
想要一个专门为您提供整个页面而不担心浪费内存,而不使用评论中建议的mmap()/ VirtualAlloc()吗? 这里就是它:
int ret;
void *ptr = NULL;
size_t page_size = sysconf(_SC_PAGESIZE);

ret = posix_memalign(&ptr, page_size, page_size);
if (ret != 0 || ptr == NULL) {
    fprintf(stderr, "posix_memalign failed: %s\n", strerror(ret));
}

顺带一提,这可能涉及微优化。你可能还没有检查你的Node是否具有缓存行的倍数大小,也没有找到提高缓存局部性的方法,也没有找到减少内存碎片化的方法。因此,你可能走错了方向:先让它工作,然后进行剖析,优化你的算法,再进行微调。


这样不会跨越两页吗?至少malloc的簿记将在前一页的末尾,对吧?我想如果你认为访问它很少见(即仅在释放时),因此不重要,那么这可能会像期望的那样起作用。 - jschultz410
1
是的,调用posix_memalign(&ptr,page_size,page_size)很可能会转换为分配2个内存页面,但是剩余部分可以由malloc()用于满足较小的分配请求。 - Yann Droneaud

3

C11标准增加了aligned_alloc函数,这样你就可以做一些像这样的事情:

#include <stdlib.h>
#include <unistd.h>

void *alloc_page( void )
{
    long page_size = sysconf( _SC_PAGESIZE );  /* arguably could be a constant, #define, etc. */

    return ( page_size > 0 ? aligned_alloc( page_size, page_size ) : NULL );
}

这种方法的问题,正如其他人所指出的那样,通常标准分配调用的实现会添加一些簿记开销,这些开销存储在所分配内存之前。因此,这个分配通常会跨越两页:返回给你使用的页面以及分配器簿记使用的另一页的结尾。
这意味着当您释放或重新分配此内存时,可能需要触及两个页面而不仅仅是一个页面。另外,如果您以这种方式分配所有或大部分内存,则可以“浪费”大量虚拟内存,因为在操作系统级别分配给您的进程的页面中,大约有一半将只对分配器的簿记使用很少的部分进行使用。
这些问题的重要性通常很难确定,但最好能以某种方式避免它们。不幸的是,我还没有找到一种干净、简单和可移植的方法来做到这一点。
补充说明:如果您能动态地确定malloc的内存开销并假设它始终是恒定的,那么请求较少的内存是否通常会给我们带来所需的内容?
#include <stdlib.h>
#include <unistd.h>

/* decent default guesses (e.g. - Linux x64) */

static size_t Page_Size       = 4096;
static size_t Malloc_Overhead = 32;

/* call once at beginning of program (i.e. - single thread, no allocs yet) */

int alloc_page_init( void )  
{
    int     ret       = -1;
    long    page_size = sysconf( _SC_PAGESIZE );
    char   *p1        = malloc( 1 );
    char   *p2        = malloc( 1 );
    size_t  malloc_overhead;

    if ( page_size <= 0 || p1 == NULL || p2 == NULL )
        goto FAIL;

    malloc_overhead = ( size_t ) ( p2 > p1 ? p2 - p1 : p1 - p2 );  /* non-standard pointer math */

    if ( malloc_overhead > 64 || malloc_overhead >= page_size )
        goto FAIL;

    Page_Size       = page_size;
    Malloc_Overhead = malloc_overhead;
    ret             = 0;

FAIL:
    if ( p1 )
        free( p1 );

    if ( p2 )
        free( p2 );

    return ret;
}

void *alloc_page( void )
{
    return aligned_alloc( Page_Size - Malloc_Overhead, Page_Size - Malloc_Overhead );
}

答案:可能不行,因为例如,“以 POSIX 函数 posix_memalign 为例,其支持实现的要求接受任何是 2 的幂且是 sizeof(void *) 的倍数的对齐方式。基于 POSIX 的 aligned_alloc 实现继承了这些要求。”
上述代码可能不会请求一个 2 的幂次方的对齐方式,因此可能在大多数平台上失败。
标准分配函数的典型实现似乎存在这种无法避免的问题。因此,最好基于页面大小进行对齐和分配,并可能支付分配器簿记驻留在另一页的代价,或者使用特定于操作系统的调用(如 mmap)来避免此问题。

你指的是哪个页面大小?x86和大多数其他架构都支持多个页面大小。请参阅https://dev59.com/7WMl5IYBdhLWcg3wZWSD以获取有关x86的详细信息。 - Andrew Henle
@AndrewHenle 两段代码都动态地查询当前页面大小。然而,第二段基本上假设页面大小在运行期间不会改变。 - jschultz410

1
标准并不保证 malloc 拥有页面大小的概念。但是,当所需分配的大小与页面大小(或更大)相同时,分配器通常会提供整个页面。要求一个等于页面大小(或页面大小的倍数)的分配再自行细分并不会造成任何损害,只是需要额外的工作量。您可能会在一些机器/编译器/库组合上得到期望的行为,但也可能不会。如果您确实需要页面大小的分配和/或页面对齐的内存,则必须调用特定于操作系统的API来获取它。

2
C11标准添加了aligned_alloc。假设它有一个页面大小,您仍然可能需要一种特定于操作系统的方式来了解它。http://en.cppreference.com/w/c/memory/aligned_alloc - jschultz410
1
@jschultz410:知道这点很好。你应该把它发布为答案。 - Adrian McCarthy

1
如果您的问题是关于如何分配整个内存页:请使用mmap()而不是malloc()
原因:
malloc()必须始终向每个分配添加一些元数据,因此如果您执行malloc(4096),它肯定会分配多个页面。另一方面,mmap()是内核映射页面到您的地址空间的API。这是malloc()在底层使用的内容。
如果您的问题是关于正确舍入:将a上舍入为N的倍数的常用技巧是说rounded = (a + N-1)/N*N;。通过首先添加N-1,您确保除法在所有情况下都向上舍入。如果a已经是N的倍数,则添加的N-1将没有效果;在所有其他情况下,您将获得比rounded = a/N*N;多一个。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接