乐趣区

Rust内存分配器的不同行为

本文出自 Rust 内存分配器的不同行为,同步于 Rust 中文社区专栏:Rust 内存分配器的不同行为 , 本文时间:2019-01-04, 作者:Pslydhh,简介:Pslydhh
欢迎加入 Rust 中文社区, 共建 Rust 语言中文网络!欢迎向 Rust 中文社区专栏投稿, 投稿地址 , 好文在以下地方直接展示, 欢迎访问 Rust 中文论坛,QQ 群:570065685

Rust 中文社区首页
Rust 中文社区文章专栏

对于如下的代码,采用 nightly version:
use std::sync::mpsc;
use std::thread;
fn main() {
const STEPS: usize = 1000000;
thread::sleep(std::time::Duration::from_millis(10));
let now = std::time::Instant::now();

let (tx1, rx1) = mpsc::channel();
for _ in 0..STEPS {
let _ = tx1.send(1);
// }
// for _ in 0..STEPS {
assert_eq!(rx1.try_recv().unwrap(), 1);
}

let elapsed = now.elapsed();
println!(“recv duration: {} secs {} nanosecs\n”, elapsed.as_secs(), elapsed.subsec_nanos());
thread::sleep(std::time::Duration::from_millis(10000));
}
在我的 linux 上,观察输出 ”recv duration…” 之后进程 RES 的占用量:

如图注释:1824 kb
删除注释:48540 kb

直觉上来说这是令人奇怪的,因为不管先 send 1000000 个元素,再 try_recv 这 1000000 个元素,或者 send/recv 成对操作,操作完之后进程的内存占用应该是几乎一致的。
于是我提交了这个 Issue:Different behaviors of allocator in nuanced snippets
对方表示在删除注释的情况下,一开始就 send 了百万级别的对象,在进程中开辟了一块非常大的内存,于是随后的 try_recv 就不会回收内存了,这是一个几乎所有内存分配器都会做的优化,因为很可能你的程序随后就会再次使用那一大块内存。
这么说也算是一种合理的选择吧,在我们上面这样单线程程序下没什么问题,但是在多线程的情况下呢?这种对于内存的重用能不能跨线程重用呢?毕竟假如一个线程保留了一大块内存,另一个线程又保留一大块内存,那么进程本身会不会直接被 killed 呢?
我们来看与上述类似的一个例子:
use std::sync::mpsc;
use std::thread;

fn main() {
const STEPS: usize = 1000000;
thread::sleep(std::time::Duration::from_millis(10));
let now = std::time::Instant::now();

let t = thread::spawn(|| {
let t = thread::spawn(|| {
let (tx1, rx1) = mpsc::channel();
for _ in 0..STEPS {
let _ = tx1.send(1);
}
for _ in 0..STEPS {
assert_eq!(rx1.try_recv().unwrap(), 1);
}
});

t.join().unwrap();

let (tx1, rx1) = mpsc::channel();
for _ in 0..STEPS {
let _ = tx1.send(1);
}
for _ in 0..STEPS {
assert_eq!(rx1.try_recv().unwrap(), 1);
}
});

t.join().unwrap();

let (tx1, rx1) = mpsc::channel();
for _ in 0..STEPS {
let _ = tx1.send(1);
}
for _ in 0..STEPS {
assert_eq!(rx1.try_recv().unwrap(), 1);
}

let elapsed = now.elapsed();
println!(“recv duration: {} secs {} nanosecs\n”, elapsed.as_secs(), elapsed.subsec_nanos());
thread::sleep(std::time::Duration::from_millis(10000));
}
观察输出 ”recv duration…” 之后进程 RES 的占用量:
RES: 142364 kb
差不多是前面那个例子的三倍,注意本例子启用了三个线程,也就是说,每个线程已经结束,它之前保留的内存居然还未归还给 OS,即使这些内存再也不能被使用到。
到这里还只是内存不能使用。按照这个方式,有没有可能在程序合理的情况下,进程直接被 killed 呢?
我们把上面的 STEPS 改为 50000000,在我的机器上直接被 killed。如果把例子改一下,减少一个线程,那么它又能合理的运行了。
对于这个问题,对方回应称 This is nothing Rust can control。也许通过修改内存分配器能够修改它的行为?
好在对于后面这个例子,目前的 stable 版本 (rustc 1.31.1) 是能够回收内存的

退出移动版