【问题标题】:Performance problem: CPU intensive work performs better with more concurrency in Erlang性能问题:CPU 密集型工作在 Erlang 中的并发性更高,性能更好
【发布时间】:2011-10-13 10:53:57
【问题描述】:

tl;dr
当我以更高的并发性(例如 10K 与 4 次)执行 CPU 密集型任务时,我的 erlang 程序的性能会更好。为什么?


我正在使用 erlang 编写 map reduce 框架,并且正在进行性能测试。

我的地图函数是高度 CPU 密集型的(主要是纯计算)。它还需要访问一些静态数据,所以我在我的机器上有一些持久的(挥之不去的,即在应用程序的生命周期中存在)工作进程,每个进程都有一部分数据在内存中,并等待地图请求。 map 的输出被发送到 manager 进程(它向 worker 发送 map 请求),在那里执行 reduce(非常轻量级)。

无论如何,我注意到,当我立即为工作人员收到的每个地图请求生成一个新进程时,我的吞吐量得到了提高,而不是让工作人员进程自己一个接一个地同步执行地图请求(因此在其进程队列中留下了一堆地图请求,因为我一次触发了所有地图请求)。

代码sn-p:

%% When I remove the comment, I get significant performance boost (90% -> 96%)
%% spawn_link(fun()->
                %% One invocation uses around 250ms of CPU time
                do_map(Map, AssignedSet, Emit, Data),
                Manager ! {finished, JobId, self(), AssignedSet, normal},
%%       end),

与我在紧密循环中执行相同计算时相比,使用“立即生成”方法(例如,10000 个完全并行运行的 map reduce 作业)可以获得 96% 的吞吐量(效率)。当我使用“工人一个接一个”的方法时,我只得到了大约 90%。

我知道 Erlang 应该擅长并发处理,我印象深刻的是,即使我一次执行 10K 个 map reduce 请求而不是 100 个等,效率也不会改变!但是,由于我只有 4 个 CPU 内核,如果我使用 4 或 5 等较低的并发性,我希望获得更好的吞吐量。

奇怪的是,我的 CPU 使用率在 2 个不同的实现中看起来非常相似(在所有内核上几乎完全固定为 100%)。性能差异相当稳定。 IE。即使我只做 100 个 map reduce 工作,使用“立即生成”方法的效率仍然约为 96%,而使用“一对一”方法时的效率约为 90%。同样,当我测试 200、500、1000、10K 作业时。

我首先怀疑工作进程队列中的排队是罪魁祸首,但即使我应该在工作进程队列中只有 25 条消息,我仍然看到性能较低。 25 条消息对于造成阻塞来说似乎很小(我正在做选择性的消息匹配,但不是进程必须将消息放回队列的方式)。

我不确定我应该如何从这里开始。我做错了什么,还是我完全错过了什么?

更新

我做了更多的测试,发现性能差异可能会根据条件消失(特别是我划分静态数据的工作进程数)。看来我还有很多东西要学!

【问题讨论】:

    标签: performance optimization concurrency erlang


    【解决方案1】:

    首先,让我评论一下这是一个非常有趣的问题。我想给你一些提示:

    • 由于 reduction,每个运行队列(shell 中的 [rq:x])发生任务切换:如果 Erlang 进程调用 BIF 或用户定义的函数,它会增加它的 em>减少计数器。在一个进程中运行 CPU 密集型代码时,它会经常增加它的减少计数器。当减少计数器达到某个阈值时,将发生进程切换。 (因此,一个生命周期较长的进程与生命周期较短的多个进程具有相同的开销:它们都具有“相同”的总减少计数器并在达到阈值时触发它,例如 one process:50,000 次减少,更多进程:5 * 10,000 次减少 = 50,000 次减少。)(运行时原因)

    • 在 4 核与 1 核上运行会有所不同:但是,时间是不同的。您的核心处于 100% 的原因是因为一个或多个核心正在/正在执行映射,而其他核心正在/正在有效地“填充”您的消息队列。当您生成映射时,“填充”消息队列的时间更少,有更多时间进行映射。显然,映射是一个比填充队列更昂贵的操作,并且给它更多的内核从而提高性能。 (时间/调整原因)

    • 如果进程正在等待(接收/调用 OTP 服务器/等),当您提高并发级别时,您将获得更高的吞吐量。例如:从静态持久化工作者请求数据需要一些时间。 (语言原因)

    【讨论】:

    • 我会说相反,一个核心将“填充”队列,而另一个核心将接收消息并执行映射,因为只有一个管理器。但实际上,管理器可能会被替换为一些映射过程。
    • 这是一个调优问题:协同调度器在更高的并发级别下肯定会更好地工作。
    【解决方案2】:

    假设 1 个工作进程有 3 个地图操作,我们有第一个变体:

      _______   _______   _______
     |   m   | |   m   | |   m   |
     |       | |       | |       |
    _|       |_|       |_|       |_
    a         a         a         r
    

    其中a 是管理任务(从消息队列中读取、调度地图等)。m 是实际地图,r 是发回结果。为每个地图生成一个进程的第二个变体:

      _________________._
     |   m              r
     |  ___________________._
     | |   m                r
     | |  _____________________._
    _|_|_|   m                  r
    a a a
    

    如您所见,管理任务 (a) 与地图 (m) 和发回结果 (r) 同时进行。

    这将使 CPU 一直忙于映射(即计算密集型)工作,而不是时不时地短暂下降。这很可能是您在吞吐量方面看到的小幅增长。

    由于您从一开始就具有相当高的并发性,因此您只会看到相对较小的吞吐量增益。将此与理论上只运行一个工作进程(如第一个变体)相比,您会看到更大的收益。

    【讨论】:

    • 是什么让您认为 ar 任务不是 CPU 密集型的(除非它们使用磁盘或网络 I/O 或类似的)?
    • ar 主要用于处理内存混洗(复制消息数据、更改进程堆指针),而 m 会使 CPU 更忙,这可能会显示为更高的百分比CPU 负载。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-07-02
    • 2019-11-11
    • 1970-01-01
    • 2016-05-17
    • 1970-01-01
    • 1970-01-01
    • 2019-05-09
    相关资源
    最近更新 更多