惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
L
LINUX DO - 最新话题
P
Proofpoint News Feed
Cyberwarzone
Cyberwarzone
Know Your Adversary
Know Your Adversary
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
L
Lohrmann on Cybersecurity
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Privacy & Cybersecurity Law Blog
K
Kaspersky official blog
The Last Watchdog
The Last Watchdog
Webroot Blog
Webroot Blog
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
WordPress大学
WordPress大学
L
LINUX DO - 热门话题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
V
Visual Studio Blog
O
OpenAI News
AI
AI
Hacker News: Ask HN
Hacker News: Ask HN
V2EX - 技术
V2EX - 技术
GbyAI
GbyAI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Simon Willison's Weblog
Simon Willison's Weblog
S
Schneier on Security
Spread Privacy
Spread Privacy
Y
Y Combinator Blog
I
InfoQ
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
F
Fortinet All Blogs
C
CERT Recently Published Vulnerability Notes
T
The Blog of Author Tim Ferriss
C
Check Point Blog
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
N
News and Events Feed by Topic
Project Zero
Project Zero
小众软件
小众软件
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
B
Blog
G
Google Developers Blog

Posts on Changkun's Blog

Why High-Output Systems Are Often the First to Stop Growing Dark Forest Theory: A Formal Derivation Agents (or Humans) in Goal-Directed and Goalless Environments: On Pipelines, Priors, and the Rhythm Between Exploration and Exploitation At the Boundary of Self-Reference: From Stable Structures in Artificial Intelligence to the Self as a Recursive Model in an Open Dissipative System 2023 Reading List 2022 Reading List 2021 Reading List Are PSS/USS and RSS Actually the Same Thing? 2020 Year-End Review Migration with Zero Downtime 2020 Reading List The All in Go Stack Pointers Might Not Be Ideal for Parameters Eliminating A Source of Measurement Errors in Benchmarks Setup Wordpress in 10 Minutes 我为什么不再写博客了? 2019 年终总结 2018-2019 读书清单 Ten years of blogging Rethinking the Reflections on Communications and Trusts 2018 年终总结 Go source code study is open source Go source study: unsafe Pattern Go source study: sync.Pool Go runtime programming A Million WebSocket and Go Designing Asynchronous RESTful APIs 分布式杂谈01:CAP 理论的误解 Issues of Human-Bot Interaction 压缩法与深度网络的泛化性 Go in 1 Hour UMSLT04: The Past and Present of SGD UMSLT03: A Gentle Start of Learning Theory UMSLT02: A Breif History of Neural Networks UMSLT01: A Breif History of Regularization 不笑不足以为道 论文笔记:Generalization in Deep Learning 2017 年终总结 2017 读书清单 深度学习的泛化理论简介 删除 GitHub 上已经提交的敏感信息 硕士生涯的第一年就这样告一段落了 人肉计算(10): 系统参与激励 人肉计算(9): 陷阱的解法 别聊,一聊你就暴露 人肉计算(8): 人肉计算与数据科学中的陷阱 人肉计算(7): 社会行为分析 Hexo + GitHub + Travis CI + VPS 自动部署 人肉计算(6): 预测市场 人肉计算(5): 信用风险评级模型 读书与回报 瞎扯: 对现代企业理论与当下IT企业的商业模式和信息产业链的规律性的思考 人肉计算(4): 输入数据聚合与PageRank 又一次打整了一下博客 人肉计算(3): 输入数据聚合与链路预测 人肉计算(2): 意图博弈 GWAPs 人肉计算(1): 众包与群众智慧 对后辈同学在计算机专业上的答疑与解惑 在德国的医疗及住院体验 这可能不是一个技术博客了 实验楼楼赛第3期-Python-题解 迅速更换了 DISQUS Electron 深度实践总结 良好的编码体验的三个方面 2016 年终总结 2016 读书清单 最近在着手写的文章 微信小程序文档极致总结 谈谈过去三个月在实验楼的实习经历 Built a Desktop Client for My Blog Guacamole 源码分析与 VNC 中 RFB 协议的坑 《高速上手 C++11/14》正式发布 Docker 极速入门教程02 - 镜像与容器管理 Docker 极速入门教程01 - 基本概念和操作 阶段性沉默 ELK+Redis 最佳实践 终于全面启用了 HTTPS 苹果开源了LZFSE无损压缩 Hash 碰撞的一种思路 记一次完整的 Kaldi-TIMIT 示例运行 Kaldi 上的 TIMIT 例子 Kaldi 安装与部署 从科研写作谈起 Swift API 设计指南 有趣的人类 所以其实论文并没有什么鬼用 Githug 通关记录及指南 小结一下这学期的收获 2015 读书清单 2015 年终总结 负能量爆表 转眼就快两个月了 博客迁移记录 大三总结 这个世界,终究不会是我们的。 Linux 内核分析 之六:Linux 内核创建进程的过程 小说「泽缘」 Linux 内核分析 之五:system_call中断处理过程的简要分析 大创项目的标题真是每年都在考验同学们的想象力啊 Linux 内核分析 之四:使用库函数API和嵌入汇编两种方式使用同一个系统调用
Performance Differences from Page Faults vs. Prefetching
Changkun Ou · 2021-01-18 · via Posts on Changkun's Blog

Published at发布于:   |   PV/UV: /   |   Reading阅读: 11 min

Just how large can the performance difference caused by page faults be? Let’s write a benchmark to find out.

Simulating Page Fault Behavior

To build this benchmark, we need to understand two low-level Linux syscalls for memory management: mmap and madvise. For memory allocation, mmap can be called with the anonymous and private mapping flags MAP_ANON and MAP_PRIVATE. Memory allocated this way is in a page-fault state — any access to the allocated region will trigger a page fault. We can exploit this to measure the cost of accessing memory through page faults. Meanwhile, madvise can advise the kernel to prefetch memory in advance. By applying prefetching to mmap-allocated memory, we can then measure the cost of accessing already-prefetched memory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main_test

import (
	"fmt"
	"syscall"
	"testing"
)

var pageSize = syscall.Getpagesize()

func BenchmarkPrefetch(b *testing.B) {
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, true)
	}
}

func BenchmarkPageFault(b *testing.B) {
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, false)
	}
}

func benchMem(b *testing.B, allocMB int, prefetch bool) {
	b.Run(fmt.Sprintf("%dMiB", allocMB), func(b *testing.B) {
		for j := 0; j < b.N; j++ {
			b.StopTimer()
			anonMB := allocMB << 20 // MiB
			m, err := syscall.Mmap(-1, 0, anonMB,
				syscall.PROT_READ|syscall.PROT_WRITE,
				syscall.MAP_ANON|syscall.MAP_PRIVATE)
			if err != nil {
				panic(err)
			}
			if prefetch {
				err = syscall.Madvise(m, syscall.MADV_HUGEPAGE)
				if err != nil {
					panic(err)
				}
			}
			b.StartTimer()
			// 逐页访问,用来测量写入成本
			for i := 0; i < len(m); i += pageSize {
				m[i] = 42
			}
			b.StopTimer()
			err = syscall.Madvise(m, syscall.MADV_DONTNEED)
			if err != nil {
				panic(err)
			}
		}
	})
}

Using the bench tool, we can run and obtain the following results:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ uname -a
Linux changkun-perflock 5.8.0-34-generic #37~20.04.2-Ubuntu SMP Thu Dec 17 14:53:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

$ bench
name                  time/op
Prefetch/1MiB-16        156µs ±0%
Prefetch/2MiB-16        315µs ±1%
Prefetch/4MiB-16        403µs ±1%
Prefetch/8MiB-16        581µs ±2%
Prefetch/16MiB-16      1000µs ±2%
Prefetch/32MiB-16      2170µs ±3%
Prefetch/64MiB-16      4450µs ±3%
Prefetch/128MiB-16     8920µs ±3%
Prefetch/256MiB-16    18200µs ±1%
Prefetch/512MiB-16    36600µs ±1%
Prefetch/1024MiB-16   72200µs ±4%
PageFault/1MiB-16       157µs ±1%
PageFault/2MiB-16       315µs ±1%
PageFault/4MiB-16       638µs ±1%
PageFault/8MiB-16      1310µs ±1%
PageFault/16MiB-16     2760µs ±1%
PageFault/32MiB-16     5940µs ±1%
PageFault/64MiB-16    12100µs ±0%
PageFault/128MiB-16   23900µs ±1%
PageFault/256MiB-16   47400µs ±1%
PageFault/512MiB-16   94100µs ±0%
PageFault/1024MiB-16 187000µs ±1%

As allocation size grows, the performance gain from prefetching becomes very significant:

MADV_DONTNEED v.s. MADV_FREE

Worth noting is that we used the MADV_DONTNEED flag to release memory. For the other release mode, MADV_FREE, its lazy-release semantics mean that memory marked for release does not immediately enter a page-fault state, which could affect subsequent memory operations and should in principle yield some performance improvement. We can verify the effect of switching to MADV_FREE with a simple test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// main.go
package main

/*
#include <sys/mman.h>
*/
import "C"

var MADV_FREE = C.MADV_FREE // 获得 MADV_FREE 参数

func main() {}

// main_test.go
package main

import (
	"fmt"
	"syscall"
	"testing"
)

var pageSize = syscall.Getpagesize()

func BenchmarkPrefetch(b *testing.B) {
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, true, true)
	}
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, true, false)
	}
}

func BenchmarkPageFault(b *testing.B) {
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, false, true)
	}
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, false, false)
	}
}

func benchMem(b *testing.B, allocMB int, prefetch bool, dontneed bool) {
	var s string
	if dontneed {
		s = fmt.Sprintf("MADV-DONTNEED-%dMiB", allocMB)
	} else {
		s = fmt.Sprintf("MADV-FREE-%dMiB", allocMB)
	}
	b.Run(s, func(b *testing.B) {
		for j := 0; j < b.N; j++ {
			b.StopTimer()
			anonMB := allocMB << 20 // MiB
			m, err := syscall.Mmap(-1, 0, anonMB, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE)
			if err != nil {
				panic(err)
			}
			if prefetch {
				err = syscall.Madvise(m, syscall.MADV_HUGEPAGE)
				if err != nil {
					panic(err)
				}
			}
			b.StartTimer()
			for i := 0; i < len(m); i += pageSize {
				m[i] = 42
			}
			b.StopTimer()
			if dontneed {
				err = syscall.Madvise(m, syscall.MADV_DONTNEED)
				if err != nil {
					panic(err)
				}
			} else {
				err = syscall.Madvise(m, MADV_FREE)
				if err != nil {
					panic(err)
				}
			}
		}
	})
}

We can obtain the following results:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name                                time/op
Prefetch/MADV-DONTNEED-1MiB-16        157µs ±1%
Prefetch/MADV-DONTNEED-2MiB-16       180µs ±77%
Prefetch/MADV-DONTNEED-4MiB-16        172µs ±1%
Prefetch/MADV-DONTNEED-8MiB-16        351µs ±5%
Prefetch/MADV-DONTNEED-16MiB-16       753µs ±4%
Prefetch/MADV-DONTNEED-32MiB-16      1.91ms ±3%
Prefetch/MADV-DONTNEED-64MiB-16      4.22ms ±3%
Prefetch/MADV-DONTNEED-128MiB-16     8.65ms ±4%
Prefetch/MADV-DONTNEED-256MiB-16     17.7ms ±3%
Prefetch/MADV-DONTNEED-512MiB-16     35.7ms ±2%
Prefetch/MADV-FREE-1MiB-16            189µs ±4%
Prefetch/MADV-FREE-2MiB-16            391µs ±4%
Prefetch/MADV-FREE-4MiB-16          1.64ms ±19%
Prefetch/MADV-FREE-8MiB-16          2.84ms ±31%
Prefetch/MADV-FREE-16MiB-16          3.32ms ±8%
Prefetch/MADV-FREE-32MiB-16          6.30ms ±1%
Prefetch/MADV-FREE-64MiB-16          12.7ms ±1%
Prefetch/MADV-FREE-128MiB-16         25.1ms ±2%
Prefetch/MADV-FREE-256MiB-16         50.7ms ±2%
Prefetch/MADV-FREE-512MiB-16          101ms ±1%
PageFault/MADV-DONTNEED-1MiB-16       157µs ±0%
PageFault/MADV-DONTNEED-2MiB-16       317µs ±1%
PageFault/MADV-DONTNEED-4MiB-16       645µs ±1%
PageFault/MADV-DONTNEED-8MiB-16      1.31ms ±1%
PageFault/MADV-DONTNEED-16MiB-16     2.77ms ±1%
PageFault/MADV-DONTNEED-32MiB-16     5.92ms ±0%
PageFault/MADV-DONTNEED-64MiB-16     12.4ms ±0%
PageFault/MADV-DONTNEED-128MiB-16    25.2ms ±0%
PageFault/MADV-DONTNEED-256MiB-16    50.7ms ±1%
PageFault/MADV-DONTNEED-512MiB-16     102ms ±0%
PageFault/MADV-FREE-1MiB-16           191µs ±2%
PageFault/MADV-FREE-2MiB-16           389µs ±3%
PageFault/MADV-FREE-4MiB-16           770µs ±1%
PageFault/MADV-FREE-8MiB-16          1.54ms ±1%
PageFault/MADV-FREE-16MiB-16         3.08ms ±1%
PageFault/MADV-FREE-32MiB-16         6.17ms ±2%
PageFault/MADV-FREE-64MiB-16         12.3ms ±2%
PageFault/MADV-FREE-128MiB-16        25.0ms ±2%
PageFault/MADV-FREE-256MiB-16        50.1ms ±3%
PageFault/MADV-FREE-512MiB-16         101ms ±1%

We can see that using MADV_FREE in the page-fault scenario brings little change:

1
2
PageFault/MADV-DONTNEED-512MiB-16     102ms ±0%
PageFault/MADV-FREE-512MiB-16         101ms ±1%

Conversely, in the prefetch scenario, MADV_FREE does not deliver the better performance it claims — instead it introduces more overhead:

1
2
Prefetch/MADV-DONTNEED-512MiB-16     35.7ms ±2%
Prefetch/MADV-FREE-512MiB-16          101ms ±1%

This is rather interesting. Why does this happen? When kernel support for MADV_FREE was added, there was a mailing list discussion about it:

1
2
3
4
5
6
7
8
9
...
MADV_FREE is about 2 time faster than MADV_DONTNEED but
it starts slow down as memory pressure is heavy compared to
DONTNEED. It's natural because MADV_FREE needs more steps to
free pages so one thing I have a mind to overcome is just
purge them if memory pressure is severe(ex, kswapd active)
rather than giving a chance to promote freeing page
from inactive LRU when madvise_free is called.
...

Further verifying these claims would likely require digging into the kernel source, but the explanation is theoretically plausible. Since MADV_FREE merely defers the release of memory, it still needs to release memory when pressure is high. Our benchmark repeatedly allocates large blocks of memory, which easily creates the appearance of heavy memory pressure — this is an extreme simulation, and actual behavior may more closely resemble the earlier page-fault test where memory pressure had not yet built up. In most cases MADV_FREE is slightly better than MADV_DONTNEED, but under high pressure MADV_FREE can be worse than MADV_DONTNEED.

Further Reading

缺页错误产生的性能差异究竟能够有多大?不妨做一个基准测试。

模拟缺页行为

想要实现这样的基准测试,需要了解 Linux 下对内存管理的两个底层的系统调用:mmapmadvise。 对于内存分配场景,mmap 可以使用匿名、私有映射两个参数 MAP_ANONMAP_PRIVATE, 这时候创建的内存实际上属于缺页状态,任何对其申请到内存区域的访问行为都将导致缺页,利用这一原理, 便可以用来测量缺页时访问内存的成本;而 madvise 能够用来给内核提供建议,提前对内存进行预取, 于是可以利用这一点,对 mmap 的来的内存执行预取操作,进而测量预取后访问内存的成本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main_test

import (
	"fmt"
	"syscall"
	"testing"
)

var pageSize = syscall.Getpagesize()

func BenchmarkPrefetch(b *testing.B) {
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, true)
	}
}

func BenchmarkPageFault(b *testing.B) {
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, false)
	}
}

func benchMem(b *testing.B, allocMB int, prefetch bool) {
	b.Run(fmt.Sprintf("%dMiB", allocMB), func(b *testing.B) {
		for j := 0; j < b.N; j++ {
			b.StopTimer()
			anonMB := allocMB << 20 // MiB
			m, err := syscall.Mmap(-1, 0, anonMB,
				syscall.PROT_READ|syscall.PROT_WRITE,
				syscall.MAP_ANON|syscall.MAP_PRIVATE)
			if err != nil {
				panic(err)
			}
			if prefetch {
				err = syscall.Madvise(m, syscall.MADV_HUGEPAGE)
				if err != nil {
					panic(err)
				}
			}
			b.StartTimer()
			// 逐页访问,用来测量写入成本
			for i := 0; i < len(m); i += pageSize {
				m[i] = 42
			}
			b.StopTimer()
			err = syscall.Madvise(m, syscall.MADV_DONTNEED)
			if err != nil {
				panic(err)
			}
		}
	})
}

使用 bench 工具,可以运行得到下面的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ uname -a
Linux changkun-perflock 5.8.0-34-generic #37~20.04.2-Ubuntu SMP Thu Dec 17 14:53:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

$ bench
name                  time/op
Prefetch/1MiB-16        156µs ±0%
Prefetch/2MiB-16        315µs ±1%
Prefetch/4MiB-16        403µs ±1%
Prefetch/8MiB-16        581µs ±2%
Prefetch/16MiB-16      1000µs ±2%
Prefetch/32MiB-16      2170µs ±3%
Prefetch/64MiB-16      4450µs ±3%
Prefetch/128MiB-16     8920µs ±3%
Prefetch/256MiB-16    18200µs ±1%
Prefetch/512MiB-16    36600µs ±1%
Prefetch/1024MiB-16   72200µs ±4%
PageFault/1MiB-16       157µs ±1%
PageFault/2MiB-16       315µs ±1%
PageFault/4MiB-16       638µs ±1%
PageFault/8MiB-16      1310µs ±1%
PageFault/16MiB-16     2760µs ±1%
PageFault/32MiB-16     5940µs ±1%
PageFault/64MiB-16    12100µs ±0%
PageFault/128MiB-16   23900µs ±1%
PageFault/256MiB-16   47400µs ±1%
PageFault/512MiB-16   94100µs ±0%
PageFault/1024MiB-16 187000µs ±1%

可以看到随着分配内存的增大,预取带来的性能提升是非常可观的:

MADV_DONTNEED v.s. MADV_FREE

值得一提的是这里使用的是 MADV_DONTNEED 参数来释放内存。对于另一种释放模式 MADV_FREE 而言,因为其本质是懒惰释放,使用这个参数宣告释放的内存不会立刻进入缺页状态,进而对后续的内存操作可能带来影响,原则上应该会带来一定的性能提升。那么,但根据同样可以简单的验证换用 MADV_FREE 参数后带来的影响:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// main.go
package main

/*
#include <sys/mman.h>
*/
import "C"

var MADV_FREE = C.MADV_FREE // 获得 MADV_FREE 参数

func main() {}

// main_test.go
package main

import (
	"fmt"
	"syscall"
	"testing"
)

var pageSize = syscall.Getpagesize()

func BenchmarkPrefetch(b *testing.B) {
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, true, true)
	}
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, true, false)
	}
}

func BenchmarkPageFault(b *testing.B) {
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, false, true)
	}
	for i := 1; i <= 1024; i *= 2 {
		benchMem(b, i, false, false)
	}
}

func benchMem(b *testing.B, allocMB int, prefetch bool, dontneed bool) {
	var s string
	if dontneed {
		s = fmt.Sprintf("MADV-DONTNEED-%dMiB", allocMB)
	} else {
		s = fmt.Sprintf("MADV-FREE-%dMiB", allocMB)
	}
	b.Run(s, func(b *testing.B) {
		for j := 0; j < b.N; j++ {
			b.StopTimer()
			anonMB := allocMB << 20 // MiB
			m, err := syscall.Mmap(-1, 0, anonMB, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE)
			if err != nil {
				panic(err)
			}
			if prefetch {
				err = syscall.Madvise(m, syscall.MADV_HUGEPAGE)
				if err != nil {
					panic(err)
				}
			}
			b.StartTimer()
			for i := 0; i < len(m); i += pageSize {
				m[i] = 42
			}
			b.StopTimer()
			if dontneed {
				err = syscall.Madvise(m, syscall.MADV_DONTNEED)
				if err != nil {
					panic(err)
				}
			} else {
				err = syscall.Madvise(m, MADV_FREE)
				if err != nil {
					panic(err)
				}
			}
		}
	})
}

同样的可以得到下面的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name                                time/op
Prefetch/MADV-DONTNEED-1MiB-16        157µs ±1%
Prefetch/MADV-DONTNEED-2MiB-16       180µs ±77%
Prefetch/MADV-DONTNEED-4MiB-16        172µs ±1%
Prefetch/MADV-DONTNEED-8MiB-16        351µs ±5%
Prefetch/MADV-DONTNEED-16MiB-16       753µs ±4%
Prefetch/MADV-DONTNEED-32MiB-16      1.91ms ±3%
Prefetch/MADV-DONTNEED-64MiB-16      4.22ms ±3%
Prefetch/MADV-DONTNEED-128MiB-16     8.65ms ±4%
Prefetch/MADV-DONTNEED-256MiB-16     17.7ms ±3%
Prefetch/MADV-DONTNEED-512MiB-16     35.7ms ±2%
Prefetch/MADV-FREE-1MiB-16            189µs ±4%
Prefetch/MADV-FREE-2MiB-16            391µs ±4%
Prefetch/MADV-FREE-4MiB-16          1.64ms ±19%
Prefetch/MADV-FREE-8MiB-16          2.84ms ±31%
Prefetch/MADV-FREE-16MiB-16          3.32ms ±8%
Prefetch/MADV-FREE-32MiB-16          6.30ms ±1%
Prefetch/MADV-FREE-64MiB-16          12.7ms ±1%
Prefetch/MADV-FREE-128MiB-16         25.1ms ±2%
Prefetch/MADV-FREE-256MiB-16         50.7ms ±2%
Prefetch/MADV-FREE-512MiB-16          101ms ±1%
PageFault/MADV-DONTNEED-1MiB-16       157µs ±0%
PageFault/MADV-DONTNEED-2MiB-16       317µs ±1%
PageFault/MADV-DONTNEED-4MiB-16       645µs ±1%
PageFault/MADV-DONTNEED-8MiB-16      1.31ms ±1%
PageFault/MADV-DONTNEED-16MiB-16     2.77ms ±1%
PageFault/MADV-DONTNEED-32MiB-16     5.92ms ±0%
PageFault/MADV-DONTNEED-64MiB-16     12.4ms ±0%
PageFault/MADV-DONTNEED-128MiB-16    25.2ms ±0%
PageFault/MADV-DONTNEED-256MiB-16    50.7ms ±1%
PageFault/MADV-DONTNEED-512MiB-16     102ms ±0%
PageFault/MADV-FREE-1MiB-16           191µs ±2%
PageFault/MADV-FREE-2MiB-16           389µs ±3%
PageFault/MADV-FREE-4MiB-16           770µs ±1%
PageFault/MADV-FREE-8MiB-16          1.54ms ±1%
PageFault/MADV-FREE-16MiB-16         3.08ms ±1%
PageFault/MADV-FREE-32MiB-16         6.17ms ±2%
PageFault/MADV-FREE-64MiB-16         12.3ms ±2%
PageFault/MADV-FREE-128MiB-16        25.0ms ±2%
PageFault/MADV-FREE-256MiB-16        50.1ms ±3%
PageFault/MADV-FREE-512MiB-16         101ms ±1%

可以看到使用 MADV_FREE 缺页场景下并没有带来多大变化:

1
2
PageFault/MADV-DONTNEED-512MiB-16     102ms ±0%
PageFault/MADV-FREE-512MiB-16         101ms ±1%

相反,对于已经预取的情况下并没有 MADV_FREE 宣称的那样具有更好的性能,反而带来了更多的性能损耗:

1
2
Prefetch/MADV-DONTNEED-512MiB-16     35.7ms ±2%
Prefetch/MADV-FREE-512MiB-16          101ms ±1%

这就比较有有趣了。为什么会这样呢?在内核增加 MADV_FREE 支持的时候有这样一个邮件列表讨论:

1
2
3
4
5
6
7
8
9
...
MADV_FREE is about 2 time faster than MADV_DONTNEED but
it starts slow down as memory pressure is heavy compared to
DONTNEED. It's natural because MADV_FREE needs more steps to
free pages so one thing I have a mind to overcome is just
purge them if memory pressure is severe(ex, kswapd active)
rather than giving a chance to promote freeing page
from inactive LRU when madvise_free is called.
...

至于如何进一步验证这些说法可能需要把内核代码拿出来溜了,不过这个解释读起来从理论上分析是比较可信的。 由于从 MADV_FREE 的行为来看只是延缓了释放的行为,实际上当内存紧张时还是需要释放的。 我们上面的基准测试反复申请内存,很容易造成内存紧张的假象,某种程度上属于极端的模拟情况, 实际状态下可能跟此前的 PageFault 测试中还未造成内存高压相似。 即大部分情况下 MADV_FREE 略好于 MADV_DONTNEED, 在面临高压时 MADV_FREE 反逊于 MADV_DONTNEED

进一步阅读的参考