mmap在共享内存中的性能是否可以提高?

4
我正在编写一个小程序,用于在Wayland中使用软件渲染和wl_shm进行显示。这需要我将屏幕缓冲区的文件描述符传递给Wayland服务器,然后服务器调用mmap(),即屏幕缓冲区必须在进程之间共享。
在这个程序中,启动延迟非常重要。目前,只剩下一个瓶颈:初始绘制屏幕缓冲区,其中整个缓冲区被涂上。下面的代码展示了这个过程的简化版本:
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    /* Fullscreen buffers are around 10-30 MiB for common resolutions. */
    const size_t size = 2880 * 1800 * 4;
    int fd = memfd_create("shm", 0);
    ftruncate(fd, size);
    void *pool = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    /* Ideally, we could just malloc, but this memory needs to be shared. */
    //void *pool = malloc(size);

    /* In reality this is a cairo_paint() call. */
    memset(pool, 0xCF, size);

    /* Subsequent paints (or memsets) after the first take negligible time. */
}

在我的笔记本电脑上,上述的memset()需要大约21-28毫秒。切换到使用malloc()分配的内存可以将这个时间降低到12毫秒,但问题是这些内存需要在进程之间共享。在我的台式机上,情况类似:使用mmap()花费7毫秒,使用malloc()花费3毫秒。

我的问题是:我是否遗漏了什么可以提高Linux上共享内存性能的东西?我尝试过使用madvise()MADV_WILLNEED以及MADV_SEQUENTIAL,以及使用mlock(),但这些都没有改善性能。我也考虑过是否使用2MB的Huge Pages会有帮助,因为缓冲区大小大约在10-30 MB左右,但通常不可用。

编辑:我尝试过使用mmap()MAP_ANONYMOUS | MAP_SHARED,速度与之前一样慢。而使用MAP_ANONYMOUS | MAP_PRIVATE则与使用malloc()速度相同,但这违背了初衷。


1
第一次还是每次都是21-28毫秒?malloc从与mmap相同的位置获取其内存,因此有差异令人惊讶。如果映射匿名内存,速度是否与malloc相同? - user253751
只有第一个; 后续的 memset() 大约需要 2 毫秒。我会编辑代码并进行澄清。 - Pjj56
1
我正在研究当设置了VM_SHARED | VM_MAYSHARE标志时有什么不同,我认为主要的区别在于这个balance_dirty_pages函数有时会在页面故障时被调用:https://elixir.bootlin.com/linux/latest/source/mm/page-writeback.c#L1557,它可能会决定减慢您的进程。我想知道如果您逐个页面地写入(每次4096字节),是否有些页面比其他页面需要更长的时间?一些信息可以在这里找到:https://lwn.net/Articles/456904/,但这个假设尚未得到证实。 - user253751
啊,那是累计的,这个是每页的:https://i.imgur.com/lXsMdyY.png。看起来似乎有一个每50-100页慢访问的模式。 - Pjj56
1
即使仅在time下运行它也会计算较小的页面错误的数量,这很可能就是时间消耗的地方。如果您使用不同数量的页面 time 运行几次,您可以轻松验证这一点。 - Useless
显示剩余13条评论
1个回答

3
malloc()mmap()之间的性能差异似乎是由于透明大页面的不同应用引起的。在x86_64上,默认页面大小为4KiB,巨大页面大小为2MiB。透明大页面允许不知道巨大页面的程序仍然使用它们,从而减少页面错误。但是,这仅对私有匿名内存启用默认值-因此对于使用MAP_ANONYMOUS | MAP_PRIVATEmalloc()mmap(),其性能相同。对于共享内存映射,这将被禁用,导致更多的页面处理开销(对于我需要的10-30MiB缓冲区),并导致减速。可以通过内核文档页中所述的方式启用共享内存映射的Hugepages,通过/sys/kernel/mm/transparent_hugepage/shmem_enabled旋钮进行操作。这默认为never,但将其设置为always(或advise,并添加相应的madvise(..., MADV_HUGEPAGE)调用)允许使用MAP_SHARED进行内存映射,使性能与malloc()的内存匹配。我不确定为什么共享内存的默认值是never。虽然不太令人满意,但现在似乎唯一的解决方案是在任何系统上使用madvise(MADV_HUGEPAGE)来提高性能,只要它们恰好将shmem_enabled设置为至少advise(或者如果它在未来被默认启用)。

既然您有如此紧迫的目标,28毫秒不够用,我假设您对系统有很多控制权,无论您的程序是否正在运行。如果用户有其他程序在运行,导致您的启动时间超过28毫秒,这会成为问题吗? - user253751
这并不是一个问题,因为这不是性能关键的;它只是一个类似于rofi/dmenu的程序启动器。我只是想确保我没有浪费性能。我一直在尝试让它尽可能快地启动,因为当你按下键盘快捷键时,它在同一帧上打开非常令人满意,而且我不想将其变成守护进程并占用后台内存。我在项目页面上放了一些巨页的简要基准测试。 - Pjj56
1
总是很高兴看到有人编写高效的软件。现代软件浪费了太多的潜在性能:“安迪所给予的,比尔却拿走了。” - user253751

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