python - 为什么实习全局字符串值会导致每个多处理进程使用更少的内存?
问题描述
我有一个 Python 3.6 数据处理任务,该任务涉及预加载一个大字典,用于按 ID 查找日期,以便在后续步骤中由多处理模块管理的子进程池使用。这个过程消耗了盒子上的大部分内存,所以我应用的一个优化是“实习”存储在字典中的字符串日期。正如我预期的那样,这将 dict 的内存占用减少了几个 GB,但它也产生了另一个意想不到的效果。
在应用实习之前,子进程在执行时会逐渐消耗越来越多的内存,我认为这是因为他们不得不将字典从全局内存逐渐复制到子进程的单独分配内存(这是运行在Linux 等受益于 fork()) 的写时复制行为。即使我没有更新子进程中的字典,看起来只读访问仍然可以通过引用计数触发写时复制。
我只是希望实习可以减少 dict 的内存占用,但实际上它也阻止了内存使用量在子进程生命周期内逐渐增加。
这是我能够构建的一个最小示例,它复制了该行为,尽管它需要一个大文件来加载和填充字典,并且在值中进行足够量的重复以确保实习提供好处。
import multiprocessing
import sys
# initialise a large dict that will be visible to all processes
# that contains a lot of repeated values
global_map = dict()
with open(sys.argv[1], 'r', encoding='utf-8') as file:
if len(sys.argv) > 2:
print('interning is on')
else:
print('interning is off')
for i, line in enumerate(file):
if i > 30000000:
break
parts = line.split('|')
if len(sys.argv) > 2:
global_map[str(i)] = sys.intern(parts[2])
else:
global_map[str(i)] = parts[2]
def read_map():
# do some nonsense processing with each value in the dict
global global_map
for i in range(30000000):
x = global_map[str(i)]
y = x + '_'
return y
print("starting processes")
process_pool = multiprocessing.Pool(processes=10)
for _ in range(10):
process_pool.apply_async(read_map)
process_pool.close()
process_pool.join()
我运行了这个脚本并进行了监控htop
以查看总内存使用情况。
实习? | 打印“启动进程”后的内存使用情况 | 之后的峰值内存使用 |
---|---|---|
不 | 7.1GB | 28.0GB |
是的 | 5.5GB | 5.6GB |
虽然我很高兴这种优化似乎一次解决了我所有的内存问题,但我想更好地了解它为什么有效。如果子进程的内存使用量下降到写时复制,那么如果我对字符串进行实习,为什么不会发生这种情况?
解决方案
该CPython
实现将内部字符串存储在一个全局对象中,该对象是一个常规 Python 字典,其中键和值都是指向字符串对象的指针。
当创建一个新的子进程时,它会获取父进程地址空间的副本,因此它们将使用带有内部字符串的缩减数据字典。
我已经使用下面的补丁编译了 Python,如您所见,两个进程都可以访问带有内部字符串的表:
测试.py:
import multiprocessing as mp
import sys
import _string
PROCS = 2
STRING = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
def worker():
proc = mp.current_process()
interned = _string.interned()
try:
idx = interned.index(STRING)
except ValueError:
s = None
else:
s = interned[idx]
print(f"{proc}: <{s}>")
def main():
sys.intern(STRING)
procs = []
for _ in range(PROCS):
p = mp.Process(target=worker)
p.start()
procs.append(p)
for p in procs:
p.join()
if __name__ == "__main__":
main()
测试:
# python test.py
<Process name='Process-1' parent=3917 started>: <https://www.youtube.com/watch?v=dQw4w9WgXcQ>
<Process name='Process-2' parent=3917 started>: <https://www.youtube.com/watch?v=dQw4w9WgXcQ>
修补:
--- Objects/unicodeobject.c 2021-05-15 15:08:05.117433926 +0100
+++ Objects/unicodeobject.c.tmp 2021-05-15 23:48:35.236152366 +0100
@@ -16230,6 +16230,11 @@
_PyUnicode_FiniEncodings(&tstate->interp->unicode.fs_codec);
}
+static PyObject *
+interned_impl(PyObject *module)
+{
+ return PyDict_Values(interned);
+}
/* A _string module, to export formatter_parser and formatter_field_name_split
to the string.Formatter class implemented in Python. */
@@ -16239,6 +16244,8 @@
METH_O, PyDoc_STR("split the argument as a field name")},
{"formatter_parser", (PyCFunction) formatter_parser,
METH_O, PyDoc_STR("parse the argument as a format string")},
+ {"interned", (PyCFunction) interned_impl,
+ METH_NOARGS, PyDoc_STR("lookup interned strings")},
{NULL, NULL}
};
您可能还想看看shared_memory模块。
参考:
推荐阅读
- python - 如何从索引开始查找第一次出现的布尔值
- c++ - 在数组中返回函数参数的函数模板
- asp.net - 声纳与 .net repos 的 jenkins 文件集成
- node.js - 更新数组后猫鼬不保存记录
- nginx - Nginx 使用 proxy_pass 将 POST 更改为 GET
- c# - 我想统一为我的精灵的颜色添加一些变化
- c# - DDD 存储库与工厂,我是在补水还是创建?
- python - Youtube 播放列表不适用于 discord.py 音乐机器人
- git - 为什么 Github Pull Request 的基础分支文件(左侧)显示的内容与 HEAD 版本不同?
- sql - 使用 SQL 查询生成 100 行随机数据