为什么非交互、占用大量内存的进程的速度取决于正在运行的进程数量(以及如何修复)?

为什么非交互、占用大量内存的进程的速度取决于正在运行的进程数量(以及如何修复)?

这似乎是一个基本问题,但我无法在任何地方找到它。我希望通过在多核计算机上运行许多内存密集型进程来获得更多吞吐量。这些进程之间不进行通信。

我会预计每个进程的完成时间大致独立于正在运行的进程数量,直到进程数量接近物理核心数量(在我的例子中为 16)。

观察完成时间逐步地曲线一直延伸到当 16 个进程同时运行时每个进程的运行速度比只有一个进程运行时慢约 3 倍。

是什么让他们放慢了脚步? (请比“上下文切换”这两个词更详细。)我能对此做些什么吗?

编辑:Michael Homer 指出我对占用大量内存的进程感兴趣,而不是占用大量 CPU 的进程。我想所有这些 CPU 共享一个内存总线,这可能是瓶颈。理想情况下,我希望某种 NUMA 架构能够将进程内存“更靠近”CPU。这是否意味着我需要寻找不同的硬件来解决这个问题?

详细信息如下:

我有一个名为 的简单脚本quickie2.py,它执行一些随机的、CPU 密集型工作。我使用 bash 命令行一次性启动 N 个进程,如下所示,有 14 个进程。

for x in 1 2 3 4 5 6 7 8 9 10 11 12 13 14; do (python quickie2.py &); done

以下是每个 N 的完成时间:

N_proc  Time to completion (sec)
1       7.29
2       7.28  7.30
3       7.27  7.28  7.38
4       7.01  7.19  7.34  7.43
5       8.41  8.94  9.51  10.27  11.73
6       7.49  7.79  7.97  10.01  10.58  10.85
7       7.71  8.72  10.22  10.43  10.81  10.81  11.42
8       10.1  10.16  10.27  10.29  10.48  10.60  10.66  10.73
9       9.94  11.20  11.27  11.35  11.61  12.43  12.46  12.99  13.53
10      9.26  12.54  12.66  12.84  12.95  13.03  13.06  13.52  13.93  13.95
11      12.46  12.48  12.65  12.74  13.69  13.92  14.14  14.39  14.40  14.69  17.13
12      13.48  13.49  13.51  13.58  13.65  13.67  14.72  14.87  14.89  14.94  15.01  15.06
13      15.47  15.51  16.72  16.79  16.79  16.91  17.00  17.45  17.75  17.78  17.86  18.14  18.48
14      15.14  15.22  16.47  16.53  16.84  17.78  18.07  19.00  19.12  19.32  19.63  19.71  19.80  19.94
15      18.05  18.18  18.58  18.69  19.84  20.70  21.82  21.93  22.13  22.44  22.63  22.81  22.92  23.23  23.23
16      20.96  21.00  21.10  21.21  22.68  22.70  22.76  22.82  24.65  24.66  25.32  25.59  26.16  26.22  26.31  26.38

编辑:顺便说一句,将进程固定在核心上会使衰退变得更糟。请参阅下面的代码清单中注释掉的行。

N_proc  Time to completion (sec) with CPU-pinning
1       6.95 
2       10.11  10.18 
4       19.11  19.11  19.12  19.12 
8       20.09  20.12  20.36  20.46  23.86  23.88  23.98  24.16 
16      20.24  22.10  22.22  22.24  26.54  26.61  26.64  26.73  26.75  26.78  26.78  26.79  29.41  29.73  29.78  29.90 

这是 htop 的屏幕截图,显示确实有 N 个(此处为 14 个)核心忙碌:

  1  [|||||||||||||||98.0%]    5  [||              5.8%]     9  [||||||||||||||100.0%]    13 [                0.0%]
  2  [||||||||||||||100.0%]    6  [||||||||||||||100.0%]     10 [||||||||||||||100.0%]    14 [||||||||||||||100.0%]
  3  [||||||||||||||100.0%]    7  [||||||||||||||100.0%]     11 [||||||||||||||100.0%]    15 [||||||||||||||100.0%]
  4  [||||||||||||||100.0%]    8  [||||||||||||||100.0%]     12 [||||||||||||||100.0%]    16 [||||||||||||||100.0%]
  Mem[|||||||||||||||||||||||||||||||||||||3952/64420MB]     Tasks: 96, 7 thr; 15 running
  Swp[                                        0/16383MB]     Load average: 5.34 3.66 2.29 
                                                             Uptime: 76 days, 06:59:39

为了完整起见,下面是执行一些工作的 Python 程序。唯一重要的是它让 CPU 保持忙碌。

# Code of quickie2.py (for completeness).

import numpy
import time

# import psutil
# psutil.Process().cpu_affinity([int(sys.argv[1])])

arena = numpy.empty(240*1024**2, dtype=numpy.uint8)

startTime = time.time()

# just do some work that takes a lot of CPU
for i in range(100):
    one = arena[:80*1024**2].view(numpy.float64)
    two = arena[80*1024**2:160*1024**2].view(numpy.float64)
    three = arena[160*1024**2:].view(numpy.float64)
    three = one + two

print(" {:.2f} ".format(time.time() - startTime))

答案1

现在我明白出了什么问题,我知道这是硬件限制,而不是 UNIX 限制,因此这里不适合发帖。然而,我想我应该添加一些结束语。

我的内存有限的独立进程确实遇到了内存带宽问题。我在 Knights Landing 处理器上重复了该过程,并学习了如何在其本地 MCDRAM 上分配 Numpy 数组。使用本地内存,内存总线上没有争用,并且该进程继续扩展,远高于我在普通硬件上观察到的限制。

事件率与独立进程数

以下是在 MCDRAM(而不是普通 RAM)上分配 Numpy 数组的方法。

import ctypes
import numpy

def malloc_mcdram(size):
    libnuma = ctypes.cdll.LoadLibrary("libnuma.so")
    assert libnuma.numa_available() == 0   # NUMA not available is -1

    libnuma.numa_alloc_onnode.restype = ctypes.POINTER(ctypes.c_uint8)
    return libnuma.numa_alloc_onnode(ctypes.c_size_t(size), ctypes.c_int(1))

def custom_allocator_array(allocator, size, dtype):
    ptr = allocator(size)
    ptr.__array_interface__ = {"version": 3,
                               "typestr": numpy.ctypeslib._dtype(type(ptr.contents)).str,
                               "data": (ctypes.addressof(ptr.contents), False),
                               "shape": (size,)}
    return numpy.array(ptr, copy=False).view(dtype)

myarray = custom_allocator_array(malloc_mcdram, sizeInBytes, numpy.float64)

答案2

你的进程是内存重的,而不是CPU重的。试试这个:

#!/usr/bin/env python

import datetime
import hashlib

data = "\0" * 64

ts_start = datetime.datetime.now()
for i in range(10000000):
    data = hashlib.sha512(data).digest()
ts_end = datetime.datetime.now()
print("Elapsed: %s" % (ts_end - ts_start))

在我的 2 插槽/8 核/16 线程机器上并行运行最多 8 个运行时,我得到了一致的结果,大约需要 20 秒才能完成。除此之外,当进程开始争夺 CPU 资源时,性能会下降。

单次运行:

~$ python cpuheavy.py 
Elapsed: 0:00:20.461652

8 个并行(= 每个核心 1 个),仍然是同一时间:

~$ for i in $(seq 8); do python cpuheavy.py & done
Elapsed: 0:00:18.979012
Elapsed: 0:00:19.092770
Elapsed: 0:00:19.873763
Elapsed: 0:00:20.139105
Elapsed: 0:00:20.147066
Elapsed: 0:00:20.181319
Elapsed: 0:00:21.328754
Elapsed: 0:00:21.495310

并行运行 16 次(= 每个超线程 1 次),随着进程开始争夺 CPU 时间,时间增加到约 31 秒。 Ca 时间增加 50%。

由于并行运行 32 个进程必须共享 CPU 线程,因此性能下降。每个流程的完成时间增加到 2 分钟以上(时间增加了 4 倍)。

相关内容