移动内存页面比mremap()更快的方法?

我一直在试验mremap()。 我希望能够高速移动虚拟内存页面。 至less比复制速度快。 我有一些可以利用能够快速移动内存页面的algorithm。 问题是,下面的程序显示,mremap()是非常慢的 – 至less在我的i7笔记本电脑 – 相比之下,实际上是逐字节地拷贝相同的内存页面。

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

问题:

为什么mremap()这么慢?

是否有另一个用户的土地或内核可调用页面映射操作API可能会更快?

是否有另一个用户地址或内核地址可调用页面映射操作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; } 

更新:所以我们不需要质疑“kernelspace”往返速度有多快,下面是一个更进一步的性能testing程序,它显示我们可以连续调用3次getpid(),每秒处理81916192次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); } 

更新2:我添加了WIP代码来调用我发现的名为modify_ldt()的函数。 手册页暗示页面操作可能是可能的。 但是,无论我尝试什么,那么函数总是返回零,当我期待它返回读取的字节数。 'man modify_ldt'表示“成功时,modify_ldt()返回实际读取的字节数(读取)或0(写入)。失败时,modify_ldt()返回-1并设置errno来指示错误。 任何想法(a)modify_ldt()是否会替代mremap()? 和(b)如何获得modify_ldt()的工作?

看起来没有比memcpy()更快的用户登陆机制来重新排序内存页面。 mremap()要慢得多,因此仅用于重新调整先前使用mmap()分配的内存区域的大小。

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

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

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

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

一起看看所有三个引用,那么我们可以看到,内核架构师迄今为止没有多少努力以有效的方式将内存页面映射公开到用户区域。 即使在内核中,页表的操作也必须使用最多三个缓慢的锁来完成。

往前看,由于页表本身由4k页面组成,因此可以改变内核,使得特定的页面表页面对于特定的线程是唯一的,并且可以假定在页面持续期间具有无锁访问处理。 这将有助于通过用户地对该特定的页面表页面进行非常有效的操作。 但是这超出了原始问题的范围。

是什么让你认为mremap可以有效地交换单个4k页面? 至少,往返于kernelspace甚至只是读取一个值(比如pid)并返回它将比移动4k数据花费更多。 而这之前,我们得到缓存失效/ TLB重新映射内存的成本,我不明白这个答案,但应该有一些严重的成本解决不够好。

mremap基本上是一件有用的事情:为由mmap提供服务的大型分配实现realloc 。 大概,我的意思大概至少有10万。