我需要在Linux中清除大地址范围(一次200页的顺序)。我尝试了两种方法 -
使用memset
- 清除地址范围的最简单方法。执行速度比方法2慢一点。
使用munmap
/ mmap
- 我在地址范围内调用munmap
,然后mmap
再次使用相同的权限调用相同的地址。由于传递了MAP_ANONYMOUS
,因此页面将被清除。
第二种方法使基准测试运行速度提高5-10%。该基准测试不仅仅是清除页面。 如果我理解正确,这是因为操作系统有一个零页面池,它映射到地址范围。
但我不喜欢这种方式,因为munmap
和mmap
不是原子的。在某种意义上,同时完成另一个mmap
(以NULL作为第一个参数)可能会使我的地址范围无法使用。
所以我的问题是Linux是否提供了一个系统调用,可以将物理页面换成零页的地址范围?
我试着查看glibc的来源(特别是memset
),看看他们是否使用任何技术来有效地做到这一点。但我找不到任何东西。
答案 0 :(得分:5)
memset()
似乎比mmap()
快一个数量级,以获得一个新的零填充页面,至少在我现在可以访问的Solaris 11服务器上。我强烈怀疑Linux会产生类似的结果。
我写了一个小基准程序:
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <strings.h>
#include <sys/time.h>
#define NUM_BLOCKS ( 512 * 1024 )
#define BLOCKSIZE ( 4 * 1024 )
int main( int argc, char **argv )
{
int ii;
char *blocks[ NUM_BLOCKS ];
hrtime_t start = gethrtime();
for ( ii = 0; ii < NUM_BLOCKS; ii++ )
{
blocks[ ii ] = mmap( NULL, BLOCKSIZE,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
// force the creation of the mapping
blocks[ ii ][ ii % BLOCKSIZE ] = ii;
}
printf( "setup time: %lf sec\n",
( gethrtime() - start ) / 1000000000.0 );
for ( int jj = 0; jj < 4; jj++ )
{
start = gethrtime();
for ( ii = 0; ii < NUM_BLOCKS; ii++ )
{
blocks[ ii ] = mmap( blocks[ ii ],
BLOCKSIZE, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
blocks[ ii ][ ii % BLOCKSIZE ] = 0;
}
printf( "mmap() time: %lf sec\n",
( gethrtime() - start ) / 1000000000.0 );
start = gethrtime();
for ( ii = 0; ii < NUM_BLOCKS; ii++ )
{
memset( blocks[ ii ], 0, BLOCKSIZE );
}
printf( "memset() time: %lf sec\n",
( gethrtime() - start ) / 1000000000.0 );
}
return( 0 );
}
请注意,在页面中的任何位置写入单个字节都是强制创建物理页面所需的全部内容。
我在我的Solaris 11文件服务器上运行它(我现在在裸机上运行的唯一POSIX风格的系统)。我没有在我的Solaris系统上测试madvise()
,因为与Linux不同,Solaris不保证映射将重新填充零填充页面,只保证“系统开始释放资源”。
结果:
setup time: 11.144852 sec
mmap() time: 15.159650 sec
memset() time: 1.817739 sec
mmap() time: 15.029283 sec
memset() time: 1.788925 sec
mmap() time: 15.083473 sec
memset() time: 1.780283 sec
mmap() time: 15.201085 sec
memset() time: 1.771827 sec
memset()
几乎快了一个数量级。当我有机会时,我会在Linux上重新运行该基准测试,但它可能必须在VM(AWS等)上。
不令人惊讶 - mmap()
价格昂贵,内核仍然需要在某个时候将页面归零。
有趣的是,评论出一行
for ( ii = 0; ii < NUM_BLOCKS; ii++ )
{
blocks[ ii ] = mmap( blocks[ ii ],
BLOCKSIZE, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
//blocks[ ii ][ ii % BLOCKSIZE ] = 0;
}
产生这些结果:
setup time: 10.962788 sec
mmap() time: 7.524939 sec
memset() time: 10.418480 sec
mmap() time: 7.512086 sec
memset() time: 10.406675 sec
mmap() time: 7.457512 sec
memset() time: 10.296231 sec
mmap() time: 7.420942 sec
memset() time: 10.414861 sec
强制创建物理映射的负担已转移到memset()
调用,在测试循环中只留下隐式munmap()
,其中映射在MAP_FIXED
时被销毁mmap()
来电取代了他们。请注意,只有munmap()
所需的时间比将地址空间保留在地址空间中的时间长3-4倍,memset()
将它们变为零。
mmap()
的成本实际上不是mmap()
/ munmap()
系统调用本身,而是新页面需要大量的幕后CPU周期才能创建实际的物理映射,并且在mmap()
系统调用本身中不会发生 - 当进程访问内存页时,它会发生之后。
如果您对结果有疑问,请注意this LMKL post from Linus Torvalds himself:
...
但是,使用虚拟内存映射玩游戏非常有用 本身很贵。它有许多非常不利的缺点 人们往往会忽视,因为记忆复制被认为是非常重要的东西 缓慢,有时优化该副本被视为一个显而易见的 改良效果。
下行到mmap:
- 非常明显的设置和拆卸成本。我的意思是明显的。这就像跟随页面表一样干净地取消映射。它是用于维护所有映射列表的簿记。这是取消映射后需要的TLB刷新。
- ...
使用Solaris Studio's collect和analyzer tools分析代码会产生以下输出:
Source File: mm.c
Inclusive Inclusive Inclusive
Total CPU Time Sync Wait Time Sync Wait Count Name
sec. sec.
1. #include <stdio.h>
2. #include <sys/mman.h>
3. #include <string.h>
4. #include <strings.h>
5.
6. #include <sys/time.h>
7.
8. #define NUM_BLOCKS ( 512 * 1024 )
9. #define BLOCKSIZE ( 4 * 1024 )
10.
11. int main( int argc, char **argv )
<Function: main>
0.011 0. 0 12. {
13. int ii;
14.
15. char *blocks[ NUM_BLOCKS ];
16.
0. 0. 0 17. hrtime_t start = gethrtime();
18.
0.129 0. 0 19. for ( ii = 0; ii < NUM_BLOCKS; ii++ )
20. {
21. blocks[ ii ] = mmap( NULL, BLOCKSIZE,
22. PROT_READ | PROT_WRITE,
3.874 0. 0 23. MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
24. // force the creation of the mapping
7.928 0. 0 25. blocks[ ii ][ ii % BLOCKSIZE ] = ii;
26. }
27.
28. printf( "setup time: %lf sec\n",
0. 0. 0 29. ( gethrtime() - start ) / 1000000000.0 );
30.
0. 0. 0 31. for ( int jj = 0; jj < 4; jj++ )
32. {
0. 0. 0 33. start = gethrtime();
34.
0.560 0. 0 35. for ( ii = 0; ii < NUM_BLOCKS; ii++ )
36. {
37. blocks[ ii ] = mmap( blocks[ ii ],
38. BLOCKSIZE, PROT_READ | PROT_WRITE,
33.432 0. 0 39. MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0 );
29.535 0. 0 40. blocks[ ii ][ ii % BLOCKSIZE ] = 0;
41. }
42.
43. printf( "mmap() time: %lf sec\n",
0. 0. 0 44. ( gethrtime() - start ) / 1000000000.0 );
0. 0. 0 45. start = gethrtime();
46.
0.101 0. 0 47. for ( ii = 0; ii < NUM_BLOCKS; ii++ )
48. {
7.362 0. 0 49. memset( blocks[ ii ], 0, BLOCKSIZE );
50. }
51.
52. printf( "memset() time: %lf sec\n",
0. 0. 0 53. ( gethrtime() - start ) / 1000000000.0 );
54. }
55.
0. 0. 0 56. return( 0 );
0. 0. 0 57. }
Compile flags: /opt/SUNWspro/bin/cc -g -m64 mm.c -W0,-xp.XAAjaAFbs71a00k.
请注意mmap()
花费的大量时间,以及每个新映射页面中单个字节的设置。
这是analyzer
工具的概述。请注意大量的系统时间:
消耗的大量系统时间是映射和取消映射物理页面所需的时间。
此时间轴显示何时消耗所有时间:
浅绿色是系统时间 - 全部在mmap()
循环中。您可以看到在memset()
循环运行时切换到深绿色用户时间。我已经突出显示了其中一个实例,以便您可以看到当时正在发生的事情。
更新了Linux VM的结果:
setup time: 2.567396 sec
mmap() time: 2.971756 sec
memset() time: 0.654947 sec
mmap() time: 3.149629 sec
memset() time: 0.658858 sec
mmap() time: 2.800389 sec
memset() time: 0.647367 sec
mmap() time: 2.915774 sec
memset() time: 0.646539 sec
这与我在昨天的评论中所说的完全一致: FWIW,我运行的快速测试显示对memset()
的简单单线程调用比重做快5到10倍mmap()
我根本不理解这种对mmap()
的迷恋。 mmap()
是一个非常昂贵的调用,它是一个强制的单线程操作 - 机器上只有一组物理内存。 mmap()
不仅 S-L-O-W ,它还会影响整个主机上的整个进程地址空间和VM系统。
使用任何形式的mmap()
只是将内存页面清零是适得其反的。首先,页面不会被免费归零 - 必须memset()
来清除它们。添加拆除并重新创建到memset()
的内存映射只是为了清除RAM页面是没有任何意义的。
memset()
还具有以下优势:多个线程可以在任何时候清除内存。更改内存映射是一个单线程进程。
答案 1 :(得分:2)
if (this.state.thumbnailsAtivas.includes(element)) {
console.log("already added")
}
else if (this.state.thumbnailsAtivas.map(thumbnail => {
thumbnail.label.includes(element.label)
})) {
console.log("label already selected")
}
else {
this.setState({thumbnailsAtivas: [...this.state.thumbnailsAtivas, textura]},
() => console.log(this.state.thumbnailsAtivas));
}
};
应该等同于Linux上匿名映射的munmap / mmap。这有点奇怪,因为这不是我理解“不需要”的语义应该是什么,但它确实丢弃了Linux上的页面。
madvise(..., MADV_DOTNEED)
$ cat > foo.c
#include <sys/types.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int
main(int argc, char **argv)
{
int *foo = mmap(NULL, getpagesize(), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
*foo = 42;
printf("%d\n", *foo);
madvise(foo, getpagesize(), MADV_DONTNEED);
printf("%d\n", *foo);
return 0;
}
$ cc -o foo foo.c && ./foo
42
0
$ uname -sr
Linux 3.10.0-693.11.6.el7.x86_64
在其他操作系统上没有这样做,所以这绝对不是可移植的。例如:
MADV_DONTNEED
但是,您不需要取消映射,您只需覆盖映射即可。作为奖励,这更便携:
$ cc -o foo foo.c && ./foo
42
42
$ uname -sr
Darwin 17.5.0
另外,我真的不确定你是否正确地对事情进行了基准测试。创建和删除映射可能非常昂贵,我不认为空闲归零会对此有所帮助。新的mmap:ed页面在第一次使用之前实际上没有映射,而在Linux上这意味着写入而不是读取,因为如果对页面的第一次访问是读取,则Linux在写入时写入零页面时会发生愚蠢的事情一个写。因此,除非你对新的mmap:ed页面进行基准测试,否则我怀疑你之前的解决方案和我在这里提出的解决方案实际上都不会比一个愚蠢的memset更快。
答案 2 :(得分:1)
注意:这是不是的答案,我只需要格式化功能。
顺便说一句:有可能/dev/zero
全零页面甚至不存在,而.read()
方法的实现方式如下(类似事件发生在dev/null
,它只返回长度参数):
struct file_operations {
...
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
...
};
static ssize_t zero_read (struct file *not_needed_here, char __user * buff, size_t len, loff_t * ignored)
{
memset (buff, 0, len);
return len;
}