0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

不同语言运行100万个并发任务需要多少内存?

算法与数据结构 来源:博客园 2023-07-12 14:14 次阅读

来自:博客园,译者:InCerry

正文

在这篇博客文章中,我深入探讨了异步和多线程编程在内存消耗方面的比较,跨足了如Rust、Go、JavaC#PythonNode.js 和 Elixir等流行语言。

不久前,我不得不对几个计算机程序进行性能比较,这些程序旨在处理大量的网络连接。我发现那些程序在内存消耗方面有巨大的差异,甚至超过20倍。有些程序在10000个连接中仅消耗了略高于100MB的内存,但另一些程序却达到了接近3GB。

不幸的是,这些程序相当复杂,功能也不尽相同,因此很难直接进行比较并得出有意义的结论,因为这不是一个典型的苹果到苹果的比较。这促使我想出了创建一个综合性基准测试的想法。

基准测试

我使用各种编程语言创建了以下程序:

启动N个并发任务,每个任务等待10秒钟,然后在所有任务完成后程序就退出。任务的数量由命令行参数控制。

在ChatGPT的小小帮助下,我可以在几分钟内用各种编程语言编写出这样的程序,甚至包括那些我不是每天都在用的编程语言。为了方便起见,所有基准测试代码都可以在我的GitHub上找到。

Rust

我用Rust编写了3个程序。第一个程序使用了传统的线程。以下是它的核心部分:

letmuthandles=Vec::new();
for_in0..num_threads{
lethandle=thread::spawn(||{
thread::from_secs(10));
});
handles.push(handle);
}
forhandleinhandles{
handle.join().unwrap();
}

另外两个版本使用了async,一个使用tokio,另一个使用async-std。以下是使用tokio的版本的核心部分:

letmuttasks=Vec::new();
for_in0..num_tasks{
tasks.push(task::spawn(async{
time::from_secs(10)).await;
}));
}
fortaskintasks{
task.await.unwrap();
}

async-std版本与此非常相似,因此我在这里就不再引用了。

Go

在Go语言中,goroutine是实现并发的基本构建块。我们不需要分开等待它们,而是使用WaitGroup来代替:

varwgsync.WaitGroup
fori:=0;i< numRoutines; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(10 * time.Second)
    }()
}
wg.Wait()

Java

Java传统上使用线程,但JDK 21提供了虚拟线程的预览,这是一个类似于goroutine的概念。因此,我创建了两个版本的基准测试。我也很好奇Java线程与Rust线程的比较。

Listthreads=newArrayList<>();
for(inti=0;i< numTasks; i++) {
    Thread thread = new Thread(() ->{
try{
Thread.sleep(Duration.ofSeconds(10));
}catch(InterruptedExceptione){
}
});
thread.start();
threads.add(thread);
}
for(Threadthread:threads){
thread.join();
}

下面是使用虚拟线程的版本。注意看它是多么的相似!几乎一模一样!

Listthreads=newArrayList<>();
for(inti=0;i< numTasks; i++) {
    Thread thread = Thread.startVirtualThread(() ->{
try{
Thread.sleep(Duration.ofSeconds(10));
}catch(InterruptedExceptione){
}
});
threads.add(thread);
}
for(Threadthread:threads){
thread.join();
}

C#

与Rust类似,C#对async/await也有一流的支持:

Listtasks=newList();
for(inti=0;i< numTasks; i++)
{
    Task task = Task.Run(async () =>
{
awaitTask.Delay(TimeSpan.FromSeconds(10));
});
tasks.Add(task);
}
awaitTask.WhenAll(tasks);

Node.JS

下面是 Node.JS:

constdelay=util.promisify(setTimeout);
consttasks=[];

for(leti=0;i< numTasks; i++) {
    tasks.push(delay(10000);
}

await Promise.all(tasks);

Python

还有Python 3.5版本中加入了async/await,所以可以这样写:

asyncdefperform_task():
awaitasyncio.sleep(10)


tasks=[]

fortask_idinrange(num_tasks):
task=asyncio.create_task(perform_task())
tasks.append(task)

awaitasyncio.gather(*tasks)

Elixir

Elixir 也因其异步功能而闻名:

tasks=
for_<- 1..num_tasks do
        Task.async(fn ->
:timer.sleep(10000)
end)
end

Task.await_many(tasks,:infinity)

测试环境

硬件: Intel(R) Xeon(R) CPU E3-1505M v6 @ 3.00GHz

操作系统: Ubuntu 22.04 LTS, Linux p5520 5.15.0-72-generic

Rust: 1.69

Go: 1.18.1

Java: OpenJDK “21-ea” build 21-ea+22-1890

.NET: 6.0.116

Node.JS: v12.22.9

Python: 3.10.6

Elixir: Erlang/OTP 24 erts-12.2.1, Elixir 1.12.2

所有程序在可用的情况下都使用发布模式(release mode)进行运行。其他选项保持为默认设置。

结果

最小内存占用

让我们从一些小的任务开始。因为某些运行时需要为自己分配一些内存,所以我们首先只启动一个任务。

adaae568-2062-11ee-962d-dac502259ad0.png

图1:启动一个任务所需的峰值内存

我们可以看到,这些程序确实分为两组。Go和Rust程序,静态编译为本地可执行文件,需要很少的内存。其他在托管平台上运行或通过解释器消耗更多内存的程序,尽管在这种情况下Python表现得相当好。这两组之间的内存消耗差距大约有一个数量级。

让我感到惊讶的是,.NET某种程度上具有最差的内存占用,但我猜这可以通过某些设置进行调整。如果有任何技巧,请在评论中告诉我。在调试模式和发布模式之间,我没有看到太大的区别。

10k 任务

add0b8c4-2062-11ee-962d-dac502259ad0.png

图2:启动10,000个任务所需的峰值内存

这里有一些意外发现!大家可能都预计线程将成为这个基准测试的大输家。这对于Java线程确实如此,实际上它们消耗了将近250MB的内存。但是从Rust中使用的原生Linux线程似乎足够轻量级,在10000个线程时,内存消耗仍然低于许多其他运行时的空闲内存消耗。异步任务或虚拟(绿色)线程可能比原生线程更轻,但我们在只有10000个任务时看不到这种优势。我们需要更多的任务。

另一个意外之处是Go。Goroutines应该非常轻量,但实际上,它们消耗的内存超过了Rust线程所需的50%。坦率地说,我本以为Go的优势会更大。因此,我认为在10000个并发任务中,线程仍然是相当有竞争力的替代方案。Linux内核在这方面肯定做得很好。

Go也失去了它在上一个基准测试中相对于Rust异步所占据的微小优势,现在它比最好的Rust程序消耗的内存多出6倍以上。它还被Python超越。

最后一个意外之处是,在10000个任务时,.NET的内存消耗并没有从空闲内存使用中显著增加。可能它只是使用了预分配的内存。或者它的空闲内存使用如此高,10000个任务太少以至于不重要。

100k 任务

我无法在我的系统上启动100,000个线程,所以线程基准测试必须被排除。可能这可以通过某种方式调整系统设置来实现,但尝试了一个小时后,我放弃了。所以在100,000个任务时,你可能不想使用线程。

ade99c18-2062-11ee-962d-dac502259ad0.png

在这一点上,Go程序不仅被Rust击败,还被Java、C#和Node.JS击败。

而Linux .NET可能有作弊,因为它的内存使用仍然没有增加。 我不得不仔细检查一下是否确实启动了正确数量的任务,果然,它确实做到了。而且它在大约10秒后仍然可以退出,所以它没有阻塞主循环。神奇!.NET干得好。

100万任务

现在我们来试试极限场景。

在100万个任务时,Elixir放弃了,提示 ** (SystemLimitError) a system limit has been reached。编辑:有些评论者指出我可以增加进程限制。在elixir启动参数中添加--erl '+P 1000000'后,它运行得很好。

ae05cf00-2062-11ee-962d-dac502259ad0.png

图4:启动100万个任务所需的峰值内存

终于我们看到了C#程序内存消耗的增加。但它仍然非常具有竞争力。它甚至成功地稍稍击败了Rust的一个运行时!

Go与其他程序之间的差距扩大了。现在Go在胜利者面前输掉了超过12倍。它还输给了Java 2倍以上,这与人们普遍认为JVM是内存大户,而Go轻量的观念相矛盾。

Rust的tokio依然无可匹敌。在看过它在100k任务下的表现后,这并不令人惊讶。

最后的话

正如我们观察到的,大量的并发任务可能会消耗大量的内存,即使它们不执行复杂的操作。不同的编程语言运行时具有不同的取舍,有些在少量任务中表现轻量和高效,但在数十万个任务中的扩展性表现差。相反,其他一些具有高初始开销的运行时可以毫不费力地应对高负载。值得注意的是,并非所有运行时都能在默认设置下处理大量的并发任务。

这个比较仅关注内存消耗,而任务启动时间和通信速度等其他因素同样重要。值得注意的是,在100万个任务时,我观察到启动任务的开销变得明显,大多数程序需要超过12秒才能完成。敬请期待即将到来的基准测试,我将深入探讨其他方面。

评论区

评论区也有很多大佬给出了建议,比较有意思,所以也翻译了放在下方

JB-Dev

在C#实现中,你不需要调用Task.Run(...)。这会增加第二个任务延续的开销。

在没有额外开销的情况下,我观察到1M基准测试的内存使用量减少了一半以上,从428MB降到183MB(代码在这里:https://github.com/J-Bax/CS...

例如,不要使用Task.Run(...)而是这样做:

tasks.Add(Task.Delay(TimeSpan.FromMilliseconds(delayMillisec)));

Christoph Berger

Go的结果并不特别令人惊讶。

每个goroutine开始时的预分配栈为2KiB,所以一百万个goroutine消耗大约2GB(2,048 * 1,000,000字节)。

这与我使用go build&& /usr/bin/time -l ./goroutinememorybenchmark运行测试代码时得到的数字非常接近:

2044968960的最大常驻集大小

(我不确定图中的2,658 GB是如何测量出来的,但数量级是相同的。)

毫无疑问,为每个goroutine预分配一个栈使Go在与那些在真正需要时才分配任何线程本地内存的并发系统的语言相比处于劣势。(附注:在这特定的上下文中,我将“线程”作为绿色或虚拟线程和goroutine的同义词。)

我想,在线程做有实质性工作的测试中,各种语言之间的差异可能会大大缩小。

D. Christoph Berge

我不完全了解后面发生了什么,但对于1000个Goroutine,Go只消耗每个goroutine约300字节。只有当它增加到每个3000 Goroutine时,它才开始每个Goroutine使用2KB。

Berger D.

你是如何得到这些测量结果的?

不幸的是,我无法复制你的发现。

为了获得较小数量的goroutine的更准确结果,我决定在每次测试运行结束后读取runtime.MemStats.StackInuse。

结果:

所有1000个goroutines完成。StackInuse:2,359,296(每个goroutine 2KB)

所有3000个goroutines完成。StackInuse:6,586,368(每个goroutine 2KB)

所有10000个goroutines完成。StackInuse:21,037,056(每个goroutine 2KB)

所有100000个goroutines完成。StackInuse:205,127,680(每个goroutine 2KB)

所有1000000个goroutines完成。StackInuse:2,048,622,592(每个goroutine 2KB)

Witek

对于Elixir / Erlang,默认进程限制为32k pids。可以通过解释器标志将其增加到20亿。例如:erl +P 4000000,我编写了一个小的Erlang程序来做你所做的事情(但确保在循环中不分配不必要的内存),并且在1百万个进程中峰值RSS使用量为2.7GiB。然而,这仍然非常人工和合成。Erlang默认为每个进程分配额外的堆,因为在现实生活中,您实际上会在进程中执行一些操作并需要一点内存,因此预先分配比以后分配更快。如果您真的想在这个愚蠢的基准测试中减少内存使用量,您可以传递选项以spawn_opt,或使用自定义+h选项启动解释器,例如。+h 10,或者+hms10(默认值为〜356)。这将将峰值RSS使用率从2.7 GiB降低到1.1 GiB。

审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 计算机
    +关注

    关注

    19

    文章

    7337

    浏览量

    87614
  • 内存
    +关注

    关注

    8

    文章

    2962

    浏览量

    73796
  • 编程语言
    +关注

    关注

    10

    文章

    1928

    浏览量

    34536
  • 程序
    +关注

    关注

    116

    文章

    3754

    浏览量

    80725
  • 多线程
    +关注

    关注

    0

    文章

    277

    浏览量

    19892

原文标题:不同语言运行100万个并发任务需要多少内存?

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    鸿蒙原生应用开发-ArkTS语言基础类库多线程并发概述

    并发模型是用来实现不同应用场景中并发任务的编程模型,常见的并发模型分为基于内存共享的并发模型和基
    发表于 03-22 15:40

    鸿蒙原生应用开发-ArkTS语言基础类库多线程并发概述

    并发模型是用来实现不同应用场景中并发任务的编程模型,常见的并发模型分为基于内存共享的并发模型和基
    发表于 03-28 14:35

    为何Python运行效率低?python语言入门

    、Facebook、Redhat、Uber等都在大规模的使用Python完成各种任务!Python的用途越来越广泛,很受欢迎,必然因为其有很多优点,但是Python也拥有一大缺点,相比于C语言,Python运行
    发表于 02-01 18:47

    并发、并行、进程、线程和协程的区别

    首先是并发并发的概念是很早就有的了。最早的时候机器都是批处理系统,而且是单道批处理系统,这个系统就是很简单的线性逻辑,一批处理任务进去,完成,然后进行下面一
    发表于 08-05 08:24

    C语言内存运行时不同变量是怎样分配的

    C语言内存运行时不同变量是怎样分配的?怎样验证C语言编译后的内存地址分配是否合理?
    发表于 02-25 06:37

    Lite Actor:方舟Actor并发模型的轻量级优化

    执行的能力,如图1所示,在一时间段中可能有多个任务都处于已启动运行运行完毕之间,同时,并发单元也可以在多核设备下并行执行,可以极大地提高
    发表于 07-18 12:00

    ATC'22顶会论文RunD:高密高并发的轻量级 Serverless 安全容器运行时 | 龙蜥技术

    RunD 已经作为 Alibaba 的 Serverless 容器运行时,服务超过100 函数 ,每天调用近 40 亿次。在线统计数据表明,RunD 使得每个节点的最大部署密度已
    发表于 09-05 15:18

    移动应用高级语言开发——并发探索

    共享,对基础架构进行了轻量化处理,大幅提升了启动时间,且优化了启动内存。 ArkCompiler并发实例运行 OpenHarmony也提供了TaskPool。TaskPool是一
    发表于 08-28 17:08

    HarmonyOS使用多线程并发能力开发

    代表,不需要开发者去面对锁带来的一系列复杂偶发的问题,同时并发度也相对较高,因此得到了广泛的支持和使用,也是当前ArkTS语言选择的并发模型。 由于Actor模型的
    发表于 09-25 15:23

    100经典C语言程序

    c语言编写,c语言100经典程序,单片机的应用,开发利用用
    发表于 12-17 11:46 11次下载

    100经典C语言程序资料

    100经典C语言程序资料,感兴趣的可以看看。
    发表于 09-27 14:46 24次下载

    并发内存池项目实现

    本项目实现了一并发内存池,参考了Google的开源项目tcmalloc实现的简易版;其功能就是实现高效的多线程内存管理。由功能可知,高并发
    的头像 发表于 11-09 11:16 655次阅读
    高<b class='flag-5'>并发</b><b class='flag-5'>内存</b>池项目实现

    接口调用并发执行十任务总结

    接口调用时,接收到一列表,十元素,需要并发执行十
    的头像 发表于 11-15 10:37 405次阅读

    C语言运行环境是什么

    计算等多个领域。为了能够正确、有效地运行C语言程序,必须具备相应的运行环境。 C语言运行环境包括软件运行
    的头像 发表于 11-27 16:13 3271次阅读

    “本源悟空”全球访问量突破100,已完成14运算任务

    截至2月1日上午11时,我国第三代自主超导量子计算机“本源悟空”已为全球94国家和地区用户成功完成142233运算任务,全球远程访问“悟空”人次已突破100
    的头像 发表于 02-19 12:50 321次阅读
    “本源悟空”全球访问量突破<b class='flag-5'>100</b><b class='flag-5'>万</b>,已完成14<b class='flag-5'>万</b><b class='flag-5'>个</b>运算<b class='flag-5'>任务</b>