gpt4 book ai didi

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

转载 作者:IT王子 更新时间:2023-10-28 23:30:19 26 4
gpt4 key购买 nike

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

测试源代码是如何工作的? mmap() 256 MB RAM,比 CPU 上的缓存大。迭代 200,000 次。在每次迭代中,使用特定的交换方法交换两个随机内存页。使用基于 mremap() 的页面交换方法运行一次。使用逐字节复制交换方法再次运行并计时。事实证明,mremap() 每秒只管理 71,577 次页面交换,而逐字节复制每秒管理高达 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;
}

更新:为了让我们不必质疑“往返内核空间”有多快,这里有一个进一步的性能测试程序,它表明我们可以连续调用 getpid() 3 次,每秒 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);
}

更新 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() 如此缓慢:

"An Introduction to Intel Memory Management"是对内存页映射理论的一个很好的介绍。

"Key concepts of Intel virtual memory"更详细地展示它是如何工作的,以防您打算编写自己的操作系统:-)

"Sharing Page Tables in the Linux Kernel"展示了一些困难的 Linux 内存页面映射架构决策及其对性能的影响。

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

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

关于c - 移动内存页面的方法比 mremap() 更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11621606/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com