为什么异步版本的 TCP 回显服务器使用的内存比同步版本多 50 倍?
Why do asynchronous versions of a TCP echo server use 50x more memory than a synchronous one?
我有一个使用标准库的简单 TCP 回显服务器:
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("localhost:4321").unwrap();
loop {
let (conn, _addr) = listener.accept().unwrap();
std::io::copy(&mut &conn, &mut &conn).unwrap();
}
}
它使用了大约 11 MB 的内存:
东京
如果我将其转换为使用 tokio:
tokio = { version = "0.2.22", features = ["full"] }
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let mut listener = TcpListener::bind("localhost:4321").await.unwrap();
loop {
let (mut conn, _addr) = listener.accept().await.unwrap();
let (read, write) = &mut conn.split();
tokio::io::copy(read, write).await.unwrap();
}
}
它使用 607 MB 内存:
async_std
与async_std类似:
async-std = "1.6.2"
use async_std::net::TcpListener;
fn main() {
async_std::task::block_on(async {
let listener = TcpListener::bind("localhost:4321").await.unwrap();
loop {
let (conn, _addr) = listener.accept().await.unwrap();
async_std::io::copy(&mut &conn, &mut &conn).await.unwrap();
}
});
}
它还使用 607 MB 内存:
为什么程序的异步版本使用的内存比同步版本多 55 倍?
您应该查看 RES
列。一个用1.0MB,一个用1.6MB。
其中大部分可能是启动 tokio 运行时和它的线程池所需的恒定开销。
我在这里试过了,就像你在评论中说的那样,有几个 64MB 的块:
==> pmap -d $(pidof tokio)
3605: target/release/tokio
Address Kbytes Mode Offset Device Mapping
…
0000555b2a634000 132 rw--- 0000000000000000 000:00000 [ anon ]
00007f2fec000000 132 rw--- 0000000000000000 000:00000 [ anon ]
00007f2fec021000 65404 ----- 0000000000000000 000:00000 [ anon ]
00007f2ff0000000 132 rw--- 0000000000000000 000:00000 [ anon ]
00007f2ff0021000 65404 ----- 0000000000000000 000:00000 [ anon ]
00007f2ff4000000 132 rw--- 0000000000000000 000:00000 [ anon ]
00007f2ff4021000 65404 ----- 0000000000000000 000:00000 [ anon ]
…
这些块既不可读也不可写,因此它们没有被映射并且不使用任何内存。它们只是代表保留地址 space.
此外,如您所见,每个 65404K 块都紧跟在 132K 块之后。由于 65404+132 恰好是 65536,我怀疑这些块代表地址 space,这是保留的,以防运行时需要稍后增长这些 132K 块之一。看看几个小时和几千个连接后的情况可能会很有趣。
glibc 的 malloc 实现为每个线程分配一个新块。块的大小由编译时常量 HEAP_MAX_SIZE(Source) 指定。因为 tokio 运行时产生了多个线程
它导致了这种高虚拟内存使用率。
为避免这种情况,您可以使用 cargo build --target=x86_64-unknown-linux-musl
.
为 musl 目标编译 rust 程序
毕竟,这是 glibc 的优化,而不是 rust 或 tokio 运行时的效果。
我有一个使用标准库的简单 TCP 回显服务器:
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("localhost:4321").unwrap();
loop {
let (conn, _addr) = listener.accept().unwrap();
std::io::copy(&mut &conn, &mut &conn).unwrap();
}
}
它使用了大约 11 MB 的内存:
东京
如果我将其转换为使用 tokio:
tokio = { version = "0.2.22", features = ["full"] }
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let mut listener = TcpListener::bind("localhost:4321").await.unwrap();
loop {
let (mut conn, _addr) = listener.accept().await.unwrap();
let (read, write) = &mut conn.split();
tokio::io::copy(read, write).await.unwrap();
}
}
它使用 607 MB 内存:
async_std
与async_std类似:
async-std = "1.6.2"
use async_std::net::TcpListener;
fn main() {
async_std::task::block_on(async {
let listener = TcpListener::bind("localhost:4321").await.unwrap();
loop {
let (conn, _addr) = listener.accept().await.unwrap();
async_std::io::copy(&mut &conn, &mut &conn).await.unwrap();
}
});
}
它还使用 607 MB 内存:
为什么程序的异步版本使用的内存比同步版本多 55 倍?
您应该查看 RES
列。一个用1.0MB,一个用1.6MB。
其中大部分可能是启动 tokio 运行时和它的线程池所需的恒定开销。
我在这里试过了,就像你在评论中说的那样,有几个 64MB 的块:
==> pmap -d $(pidof tokio)
3605: target/release/tokio
Address Kbytes Mode Offset Device Mapping
…
0000555b2a634000 132 rw--- 0000000000000000 000:00000 [ anon ]
00007f2fec000000 132 rw--- 0000000000000000 000:00000 [ anon ]
00007f2fec021000 65404 ----- 0000000000000000 000:00000 [ anon ]
00007f2ff0000000 132 rw--- 0000000000000000 000:00000 [ anon ]
00007f2ff0021000 65404 ----- 0000000000000000 000:00000 [ anon ]
00007f2ff4000000 132 rw--- 0000000000000000 000:00000 [ anon ]
00007f2ff4021000 65404 ----- 0000000000000000 000:00000 [ anon ]
…
这些块既不可读也不可写,因此它们没有被映射并且不使用任何内存。它们只是代表保留地址 space.
此外,如您所见,每个 65404K 块都紧跟在 132K 块之后。由于 65404+132 恰好是 65536,我怀疑这些块代表地址 space,这是保留的,以防运行时需要稍后增长这些 132K 块之一。看看几个小时和几千个连接后的情况可能会很有趣。
glibc 的 malloc 实现为每个线程分配一个新块。块的大小由编译时常量 HEAP_MAX_SIZE(Source) 指定。因为 tokio 运行时产生了多个线程 它导致了这种高虚拟内存使用率。
为避免这种情况,您可以使用 cargo build --target=x86_64-unknown-linux-musl
.
毕竟,这是 glibc 的优化,而不是 rust 或 tokio 运行时的效果。