有没有办法在不复制数据的情况下从 C 语言的左侧缩小内存分配?

Is there a way to shrink a memory allocation from the left in C without copying data?

提问人:Ddystopia 提问时间:6/21/2023 更新时间:10/4/2023 访问量:157

问:

我知道 C 标准库允许通过使用 realloc 函数来调整内存分配的大小,如以下示例所示:

char *a = malloc(10);
char *b = realloc(a, 8);

在这种情况下,a 和 b 可能相等,并且分配实际上已从右侧缩小了两个字节。

但是,我想知道是否有办法从左侧缩小内存分配,如下所示:

char *a = malloc(10);
char *b = /* ... */;  // shrink a from the left by 2 bytes

其中 ,和 a 开头的原始 2 个字节现在可供分配器使用。这应该发生,而不必将数据复制到内存中的新位置。只需缩小分配即可。(a + 2) == b

我知道使用 realloc 从右侧缩小内存或手动将数据复制到新的、较小的分配可能是一种选择,但这些方法不适合我的需求。

有没有办法使用 C 的标准库或提供此功能的任何其他库来实现这一点?

我不是要求 100% 保证,realloc 也可以返回指向不同位置的指针,但很可能会。

提前感谢您的见解。

我可以将所有字节向左移动并尝试缩小,但这涉及复制。

c realloc 分配

评论

1赞 Jabberwocky 6/21/2023
不,标准 C 中没有这样的东西。恐怕你需要复制。
0赞 Yunnosch 6/21/2023
我认为“左”和“右”在这里没有意义。与endianess类似,误解潜伏在这里......
0赞 user694733 6/21/2023
为什么不干脆做呢?(只要你保留以后释放内存。b = a + 2a
0赞 Ddystopia 6/21/2023
@user694733 > 和 a 开头的原始 2 个字节现在可供分配器免费使用
0赞 Jabberwocky 6/21/2023
@user694733这不会释放内存。

答:

0赞 Jan Schultke 6/21/2023 #1

realloc或任何其他标准库函数都不允许你控制内存区域收缩的方向。 事实上,调用可能会从左侧收缩它,尽管这不是它通常的实现方式。realloc(a, 8)

解决方案 1 - 不要,只做指针算术realloc

不过,您可以使用指针算术手动执行此操作:

char *a = malloc(10);
char *b = a + 2;

// TODO: free(a), or
//       free(b - 2)

请注意,这是不允许的,因为必须使用与 或 返回的指针完全相同的指针进行调用。free(b)freemallocrealloc

解决方案 2 - 从右侧收缩,反向查看数组

另一种可能性是始终与数组进行反向交互。 而不是用 , 来索引它。a[i]a[end - i - 1]

int end = 10;
char *a = malloc(end);
// first byte of reverse array is at a a[9]

end = 8;
a = realloc(a, end);
// first byte is now at a[7]

这样,即使我们从右边收缩,就好像我们从左边收缩一样。

评论

2赞 Eric Postpischil 6/21/2023
Re “事实上,调用可能会从左侧收缩它”:除非所有基本类型在 C 实现中都有两个字节或更少的对齐要求,否则这是不可能的。 并且需要返回具有基本对齐的地址,因此,如果初始地址对齐,则更改了两个字节的地址将不会具有基本对齐,除非基本对齐为两个字节或更少。realloc(a, 8)mallocrealloc
0赞 Ddystopia 6/21/2023
解决方案 2 非常聪明,谢谢。也许将来会对某人有所帮助,但不适合我。
5赞 Lundin 6/21/2023 #2

当您在堆上分配某些内容时,除了用户数据之外,几乎每个实现都需要分配更多数据。你通过&朋友界面看到的字节只是用户数据。标准库/OS 实现堆分配的常用方法是首先分配段标头,然后分配数据。malloc

这意味着在段的数据部分的紧邻“左侧”(较低地址)放置标头。在标头和数据之间留出间隙没有任何意义。相反,您必须将整个内容移动到新的内存位置。


但这些方法并不适合我的需求。

这些需求究竟是什么?使用动态内存时,我们必须注意:

  • 它比在分配和初始化时静态分配的内存慢得多。
  • 它总是伴随着内存开销。标头/查找表等方面的开销,处理对齐方面的开销,堆碎片方面的开销。

例如:

char *a = malloc(10);
char *b = realloc(a, 8);

总体上可能消耗的RAM内存比比我们说的要多得多。而且几乎可以肯定的是,它的速度会慢 ~100 到 1000 倍。char c[12];

调用所涉及的执行时间开销也很可能远比 / 调用昂贵。reallocmemcpymemmove

那么,您在这里要解决的实际问题是什么?

评论

0赞 12431234123412341234123 6/21/2023
对于“小”量来说,这是正确的,但是当您的大型缓冲区之前没有存储元数据,而是存储在内核中某处的额外表中时,可能会使用专用页面。malloc()alloc()
0赞 Lundin 6/21/2023
@12431234123412341234123这一切的肮脏细节是一个非常复杂的主题,我将从解释一些操作系统的细节中过去。对于那些真正感兴趣的人,我建议查看堆的裸机库实现,因为这些实现要简单得多。
0赞 Ddystopia 6/21/2023
更接近我原来的问题是:在 Rust 中,std::alloc::RcBox<T> 具有这样的布局: 当它转换回 时,它会重新分配 的大小,复制并删除原始 。我想尝试更有效地实现它。我最初的想法是剪切 和 ,这样它就可以与 Boxed 值具有相同的布局。现在我认为这是不可能的,所以我将尝试将字节从右移到左 + reallocrust #[repr(C)] struct RcBox<T: ?Sized> { strong: Cell<usize>, weak: Cell<usize>, value: T, } Boxsize_of::<T>()valueRcBoxstrongweakvalue
0赞 Lundin 6/21/2023
@Touchme 当然,你总是可以在原始 C 标准库之上开发某种膨胀库。这就是高级语言所做的,C++ std::vector 也非常适合。因此,如果您想要这样的功能并且不关心性能,那么请使用更高级别的语言。C接近金属,速度快,内存效率高,但它也很脆,通常是邪恶的。 最初被设计为围绕各种原始 Unix API 调用的薄包装器。与大多数 C 标准库一样。malloc
2赞 12431234123412341234123 6/21/2023 #3

有了更多关于您的系统的信息,我们可以以更直接的方式回答这个问题(如果这是一个好主意是另一个问题)。

如果您使用的是 POSIX 系统,则可以使用 和 为此。这只有在您需要大量内存时才有意义,这些内存是页面大小的数倍并且值得释放。您可以使用 (check ) 保留内存。在不再需要它的前 N 个字节(N 必须大于一个页面)之后,您可以使用释放 M 个页面 (),之后仍然使用内存。mmap()munmap()mmap()man mmapmunmap()N>=M*pageSize

我为此做了一个简短的演示:

#include <stdio.h>
#include <sys/mman.h>

int main(void)
  {
    //Map pages for the memory needed
    unsigned char *t=mmap(NULL,1024*1024,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
    printf("mmap returned address %p for 1MiB\n",t);
    if(!t)
      { perror("mmap"); return -1; }
    printf("Writing to the mapped pages\n");
    t[12]='A';

    printf("unmap the first 16 KiB we don't need anymore\n",t);
    if(munmap(t,16*1024)<0)
      { perror("munmap"); return -1; }
    printf("Writing after the unmapped pages, to the still mapped pages\n");
    t[16*1024+12]='A';

    #if 0 //Set to 0 when you don't want to see the segement fault
      printf("Create segment fault\n");
      t[15]='A'; //accessing the unmapped pages will cause a segment fault or any other UB.
    #endif

    printf("unmap the rest of the memory\n");
    if(munmap(t+16*1024,(1024-16)*1024)<0)
      { perror("munmap 2"); return -1; }
    printf("Test done successfully\n");
    return 0;
  }


此演示不检查页面大小,对于真正的程序,您必须获取页面大小并确保仅映射和取消映射整个页面。当您想要增加保留内存时,您也会遇到困难(确保您不会与其他页面发生冲突,...,当您的系统可用时,也许会变得方便)。对于其他具有 MMU 的非 Posix 平台,可能也有类似的解决方案。move_pages()

2赞 led 6/22/2023 #4

编辑:正如下面的评论中提到的,我已经重写了其中的一些内容(很晚,但无论如何),尤其是关于分页和好友系统的部分非常不准确

回到主题:

尽管自己管理这样的记忆是一个有趣的想法,但我也许可以提供一些解释,为什么在大多数情况下这也是一个坏主意:

有效地管理记忆本身就是一门真正的科学。本文提供了一个很好的概述,如果您对操作系统的操作方式感兴趣,则可以进行深入探讨。

假设您有一个如下所示的内存块,其中包含先前分配的区域。

programs allocate memory

现在想象一下,一些区域不再需要并被释放(蓝色方块)。突然间,你的记忆看起来不再那么漂亮了。必须分配新的区域,释放更多的旧区域,您的记忆看起来像瑞士奶酪 - 到处都是洞 - 很快。这是内存碎片。当然 - 您可以保留一个可用内存空间列表(或类似列表),您仍然可以在其中分配,但您的实现已经变得越来越大。如果您现在尝试从左侧释放,那么您的下一个内存标头将去哪里?(在这种情况下,甚至忽略内存的压缩)

fragmented memory

更糟糕的是:如果释放内存,如何知道该可用块旁边的内存块是否仍在使用中?还有更多的管理工作要做。如果你不这样做,你的记忆可以是自由的,但仍然像这样支离破碎:

freed memory

有解决方案,但我的建议是让 malloc() 和 realloc() 完成它们的工作。用你所拥有的。

评论

0赞 12431234123412341234123 6/23/2023
当您谈论操作系统时:为什么要关心碎片化?最有可能的是,OP 使用的系统是带有使用虚拟内存的 MMU。内核可以将进程的任何地址重定向到内核“想要”到的任何页面。当然,对于用户空间来说,这有点不同,但它不再是内核(或操作系统,如果你愿意的话)的工作,而是 clib。而且我还认为有比每次不使用而处理列表更快的解决方案(这可能是最快的方法之一)。buddy-allocator
0赞 led 6/25/2023
你对碎片化的看法是对的。我主要想指出为什么自由空间管理是困难的。从页表和虚拟化开始对我来说似乎有点矫枉过正。这是不精确的,一旦有机会,我会尽快修改它。关于伙伴分配器:我的意思是 - 可能有一千种好方法可以做到这一点 - 我想一个专门的垃圾收集器可以更快地做到这一点。再次 - 一个巨大的简化。我可能也不得不改写一下。