移动内存页面的速度比mremap()更快?

sim*_*nhf 17 c linux memory tlb

我一直在尝试使用mremap().我希望能够以高速移动虚拟内存页面.至少比复制它们更快的速度.我有一些算法的想法,可以利用能够快速移动内存页面.问题是,下面的程序显示mremap()非常慢 - 至少在我的i7笔记本电脑上 - 与实际逐字节复制相同的内存页面相比.

测试源代码如何工作?mmap()256 MB的RAM比CPU高速缓存大.迭代20万次.在每次迭代时,使用特定的交换方法交换两个随机内存页面.使用基于mremap()的页面交换方法运行一次和时间.使用逐字节复制交换方法再次运行和时间.事实证明,mremap()每秒仅管理71,577次页面交换,而逐字节复制每秒管理高达287,879次页面交换.所以mremap()比逐字节复制慢4倍!

问题:

为什么mremap()这么慢?

是否有另一个user-land或kernel-land可调页面映射操作API可能更快?

是否有另一个user-land或kernel-land可调页面映射操作API允许在一次调用中重新映射多个非连续页面?

是否有任何内核扩展支持这种事情?

#include <stdio.h>
#include <string.h>
#define __USE_GNU
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/errno.h>
#include <asm/ldt.h>
#include <asm/unistd.h>    

// gcc mremap.c && perl -MTime::HiRes -e '$t1=Time::HiRes::time;system(q[TEST_MREMAP=1 ./a.out]);$t2=Time::HiRes::time;printf qq[%u per second\n],(1/($t2-$t1))*200_000;'
// page size = 4096
// allocating 256 MB
// before 0x7f8e060bd000=0
// before 0x7f8e060be000=1
// before 0x7f8e160bd000
// after  0x7f8e060bd000=41
// after  0x7f8e060be000=228
// 71577 per second

// gcc mremap.c && perl -MTime::HiRes -e '$t1=Time::HiRes::time;system(q[TEST_COPY=1 ./a.out]);$t2=Time::HiRes::time;printf qq[%u per second\n],(1/($t2-$t1))*200_000;'
// page size = 4096
// allocating 256 MB
// before 0x7f1a9efa5000=0
// before 0x7f1a9efa6000=1
// before 0x7f1aaefa5000
// sizeof(i)=8
// after  0x7f1a9efa5000=41
// after  0x7f1a9efa6000=228
// 287879 per second

// gcc mremap.c && perl -MTime::HiRes -e '$t1=Time::HiRes::time;system(q[TEST_MEMCPY=1 ./a.out]);$t2=Time::HiRes::time;printf qq[%u per second\n],(1/($t2-$t1))*200_000;'
// page size = 4096
// allocating 256 MB
// before 0x7faf7c979000=0
// before 0x7faf7c97a000=1
// before 0x7faf8c979000
// sizeof(i)=8
// after  0x7faf7c979000=41
// after  0x7faf7c97a000=228
// 441911 per second

/*
 * Algorithm:
 * - Allocate 256 MB of memory
 * - loop 200,000 times
 *   - swap a random 4k block for a random 4k block
 * Run the test twice; once for swapping using page table, once for swapping using CPU copying!
 */

#define PAGES (1024*64)

int main() {
    int PAGE_SIZE = getpagesize();
    char* m = NULL;
    unsigned char* p[PAGES];
    void* t;

    printf("page size = %d\n", PAGE_SIZE);

    printf("allocating %u MB\n", PAGE_SIZE*PAGES / 1024 / 1024);
    m = (char*)mmap(0, PAGE_SIZE*(1+PAGES), PROT_READ | PROT_WRITE, MAP_SHARED  | MAP_ANONYMOUS, -1, 0);
    t = &m[PAGES*PAGE_SIZE];
    {
        unsigned long i;
        for (i=0; i<PAGES; i++) {
            p[i] = &m[i*PAGE_SIZE];
            memset(p[i], i & 255, PAGE_SIZE);
        }
    }

    printf("before %p=%u\n", p[0], p[0][0]);
    printf("before %p=%u\n", p[1], p[1][0]);
    printf("before %p\n", t);

    if (getenv("TEST_MREMAP")) {
        unsigned i;
        for (i=0; i<200001; i++) {
            unsigned p1 = random() % PAGES;
            unsigned p2 = random() % PAGES;
    //      mremap(void *old_address, size_t old_size, size_t new_size,int flags, /* void *new_address */);
            mremap(p[p2], PAGE_SIZE, PAGE_SIZE, MREMAP_FIXED | MREMAP_MAYMOVE, t    );
            mremap(p[p1], PAGE_SIZE, PAGE_SIZE, MREMAP_FIXED | MREMAP_MAYMOVE, p[p2]);
            mremap(t    , PAGE_SIZE, PAGE_SIZE, MREMAP_FIXED | MREMAP_MAYMOVE, p[p1]); // p3 no longer exists after this!
        } /* for() */
    }
    else if (getenv("TEST_MEMCPY")) {
        unsigned long * pu[PAGES];
        unsigned long   i;
        for (i=0; i<PAGES; i++) {
            pu[i] = (unsigned long *)p[i];
        }
        printf("sizeof(i)=%lu\n", sizeof(i));
        for (i=0; i<200001; i++) {
            unsigned p1 = random() % PAGES;
            unsigned p2 = random() % PAGES;
            unsigned long * pa = pu[p1];
            unsigned long * pb = pu[p2];
            unsigned char t[PAGE_SIZE];
            //memcpy(void *dest, const void *src, size_t n);
            memcpy(t , pb, PAGE_SIZE);
            memcpy(pb, pa, PAGE_SIZE);
            memcpy(pa, t , PAGE_SIZE);
        } /* for() */
    }
    else if (getenv("TEST_MODIFY_LDT")) {
        unsigned long * pu[PAGES];
        unsigned long   i;
        for (i=0; i<PAGES; i++) {
            pu[i] = (unsigned long *)p[i];
        }
        printf("sizeof(i)=%lu\n", sizeof(i));
        // int modify_ldt(int func, void *ptr, unsigned long bytecount);
        // 
        // modify_ldt(int func, void *ptr, unsigned long bytecount);
        // modify_ldt() reads or writes the local descriptor table (ldt) for a process. The ldt is a per-process memory management table used by the i386 processor. For more information on this table, see an Intel 386 processor handbook.
        // 
        // When func is 0, modify_ldt() reads the ldt into the memory pointed to by ptr. The number of bytes read is the smaller of bytecount and the actual size of the ldt.
        // 
        // When func is 1, modify_ldt() modifies one ldt entry. ptr points to a user_desc structure and bytecount must equal the size of this structure.
        // 
        // The user_desc structure is defined in <asm/ldt.h> as:
        // 
        // struct user_desc {
        //     unsigned int  entry_number;
        //     unsigned long base_addr;
        //     unsigned int  limit;
        //     unsigned int  seg_32bit:1;
        //     unsigned int  contents:2;
        //     unsigned int  read_exec_only:1;
        //     unsigned int  limit_in_pages:1;
        //     unsigned int  seg_not_present:1;
        //     unsigned int  useable:1;
        // };
        //
        // On success, modify_ldt() returns either the actual number of bytes read (for reading) or 0 (for writing). On failure, modify_ldt() returns -1 and sets errno to indicate the error.
        unsigned char ptr[20000];
        int result;
        result = modify_ldt(0, &ptr[0], sizeof(ptr)); printf("result=%d, errno=%u\n", result, errno);
        result = syscall(__NR_modify_ldt, 0, &ptr[0], sizeof(ptr)); printf("result=%d, errno=%u\n", result, errno);
        // todo: how to get these calls returning a non-zero value?
    }
    else {
        unsigned long * pu[PAGES];
        unsigned long   i;
        for (i=0; i<PAGES; i++) {
            pu[i] = (unsigned long *)p[i];
        }
        printf("sizeof(i)=%lu\n", sizeof(i));
        for (i=0; i<200001; i++) {
            unsigned long j;
            unsigned p1 = random() % PAGES;
            unsigned p2 = random() % PAGES;
            unsigned long * pa = pu[p1];
            unsigned long * pb = pu[p2];
            unsigned long t;
            for (j=0; j<(4096/8/8); j++) {
                t = *pa; *pa ++ = *pb; *pb ++ = t;
                t = *pa; *pa ++ = *pb; *pb ++ = t;
                t = *pa; *pa ++ = *pb; *pb ++ = t;
                t = *pa; *pa ++ = *pb; *pb ++ = t;
                t = *pa; *pa ++ = *pb; *pb ++ = t;
                t = *pa; *pa ++ = *pb; *pb ++ = t;
                t = *pa; *pa ++ = *pb; *pb ++ = t;
                t = *pa; *pa ++ = *pb; *pb ++ = t;
            }
        } /* for() */
    }

    printf("after  %p=%u\n", p[0], p[0][0]);
    printf("after  %p=%u\n", p[1], p[1][0]);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

更新:所以我们不需要质疑'往返内核空间'的速度有多快,这是一个进一步的性能测试程序,它表明我们可以连续3次调用getpid(),每秒调用81,916,192次i7笔记本电脑:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

// gcc getpid.c && perl -MTime::HiRes -e '$t1=Time::HiRes::time;system(q[TEST_COPY=1 ./a.out]);$t2=Time::HiRes::time;printf qq[%u per second\n],(1/($t2-$t1))*100_000_000;'
// running_total=8545800085458
// 81916192 per second

/*
 * Algorithm:
 * - Call getpid() 100 million times.
 */

int main() {
    unsigned i;
    unsigned long running_total = 0;
    for (i=0; i<100000001; i++) {
        /*      123123123 */
        running_total += getpid();
        running_total += getpid();
        running_total += getpid();
    } /* for() */
    printf("running_total=%lu\n", running_total);
}
Run Code Online (Sandbox Code Playgroud)

更新2:我添加了WIP代码来调用我发现的名为modify_ldt()的函数.手册页提示可以进行页面操作.但是,无论我尝试什么,当我期望它返回读取的字节数时,函数总是返回零.'man modify_ldt'表示"成功时,modify_ldt()返回读取的实际字节数(用于读取)或0(用于写入).失败时,modify_ldt()返回-1并设置errno以指示错误." 任何想法(一)是否modify_ldt()将是mremap()的替代?(b)如何让modify_ldt()工作?

sim*_*nhf 17

似乎没有比memcpy()重新排序内存页面更快的用户登陆机制.mremap()速度慢得多,因此仅用于重新调整先前使用mmap()分配的内存区域.

但是我听到你说的页面表必须非常快!用户域可以每秒数百次调用内核函数!以下参考资料有助于解释为什么mremap()如此缓慢:

"英特尔内存管理简介"是对内存页面映射理论的一个很好的介绍.

"英特尔虚拟内存的关键概念"显示了如何更详细地工作,以防您计划编写自己的操作系统:-)

"在Linux内核中共享页面表"显示了一些困难的Linux内存页面映射架构决策及其对性能的影响.

同时查看所有三个引用,我们可以看到,到目前为止,内核架构师几乎没有努力以有效的方式将内存页映射公开给用户区.即使在内核中,也必须使用最多三个锁定来完成对页面表的操作.

继续前进,因为页表本身由4k页组成,所以可以更改内核,以便特定页表页对于特定线程是唯一的,并且可以假定在该页的持续时间内具有无锁访问权.处理.这将有助于通过用户土地非常有效地操纵该特定页面表页面.但这超出了原始问题的范围.

  • 第一个过期的新链接:http://static.ow.ly/docs/memory_Rt7.pdf (2认同)
  • 您应该能够使用 SSE 流内在函数,它可能比 memcpy 更快。您的内存块是 4kb 对齐的,因此您可以轻松使用 SSE/AVX/etc 来读取/写入内存,并且流内部函数将避免污染缓存(取决于内存类型、WC/WB/etc 和您拥有的硬件)。请参阅_mm_stream_load_si128。您还可以轻松展开、预取和准备 TLB。 (2认同)

R..*_*R.. 9

是什么让你认为mremap可以有效地交换单个4k页面?至少,内核空间的往返甚至只是为了读取单个值(如pid)并返回它将花费比移动4k数据更多的成本.那是在我们得到重新映射内存的缓存失效/ TLB成本之前,我不太清楚这个问题在这个答案中解决,但是应该有一些严重的成本.

mremap基本上只有一件事是有用的:实现realloc为服务的大型分配mmap.从大到大,我的意思是至少100k.

  • 请注意,glibc在用户空间中缓存`getpid`的结果.这是不幸的,实际上是错误的,因为它可能在信号处理程序和`fork`的某些设置中给出错误的结果...... (3认同)
  • 我发现了论文"在Linux内核中共享页面表"[1],它提供了非常丰富的信息,并告诉我发现remap_file_pages()比mremap()更慢.它解释了为什么mremap()如此慢(多个锁)以及抛出内核开发人员在实现虚拟内存方面必须做出的一些艰难选择.最有趣的引用是"大型共享应用程序可能会将超过一半的物理内存用于其页表." [1] http://www.linuxinsight.com/files/ols2003/mccracken-reprint.pdf (3认同)
  • 感谢您的回答,但是为什么让您认为"内核空间往返"是如此之慢,并且比移动4k数据慢?我在上面添加了一个进一步的性能测试程序,说明"往返于内核空间"应该不是问题.我们每秒可以拨打3次调用getpid()8200万次,但每秒只能交换20万次4k块. (2认同)
  • 如果你真的想让你的想法发挥作用,我认为你必须在一个系统调用中发明一个新的系统调用(或者`mremap`的新标志)来交换页面.但是,由于内核没有单独考虑页面,因此分割和合并VMA仍然会有很多重要的工作要做; 它将它们视为VMA跨度的一部分.也许有一种方法,如果它是匿名内存,你可以在不调整VMA的情况下切换它所支持的底层页面,但是为了工作,你几乎肯定需要提前锁定所有内存(`mlock`) . (2认同)