我想在python 3.7中使用trie搜索,以匹配具有某些给定单词的字符串。 特里搜索算法实际上是相当快的,但是我也想使用我的CPU拥有的所有内核。假设我的电脑有8个核心,而我想使用7个。 因此,我将单词数据库分为7个同样大的列表,并在每个列表中创建了一个trie。 (这是使代码并行化的基本思想)
但是,当我从多处理模块中调用Process()时,Process()。start()方法在真实数据库上可能要花费几秒钟。 (搜索本身大约需要一微秒)。
说实话,我还不是一个专业的程序员,这意味着我可能在代码中犯了一些重大错误。有人看到进程开始如此缓慢的原因吗?
请考虑我使用比下面的Trie更大的数据库来测试脚本。我还测试了脚本,每次仅调用1个进程,而且速度也大大降低。 我想提供更少的代码,但是我认为很高兴看到运行中的问题。如果需要,我还可以提供其他信息。
import string
import sys
import time
from multiprocessing import Process, Manager
from itertools import combinations_with_replacement
class TrieNode:
def __init__(self):
self.isString = False
self.children = {}
def insertString(self, word, root):
currentNode = root
for char in word:
if char not in currentNode.children:
currentNode.children[char] = TrieNode()
currentNode = currentNode.children[char]
currentNode.isString = True
def findStrings(self, prefix, node, results):
# Hänge das Ergebnis an, wenn ein Ende gefunden wurde
if node.isString:
results.append(prefix)
for char in node.children:
self.findStrings(prefix + char, node.children[char], results)
def findSubStrings(self, start_prefix, root, results):
currentNode = root
for char in start_prefix:
# Beende Schleife auf fehlende Prefixes oder deren Kinder
if char not in currentNode.children:
break
# Wechsle zu Kindern in anderem Falle
else:
currentNode = currentNode.children[char]
# Verwende findStrings Rekursiv zum auffinden von End-Knoten
self.findStrings(start_prefix, currentNode, results)
return results
def gen_word_list(num_words, min_word_len=4, max_word_len=10):
wordList = []
total_words = 0
for long_word in combinations_with_replacement(string.ascii_lowercase, max_word_len):
wordList.append(long_word)
total_words += 1
if total_words >= num_words:
break
for cut_length in range(1, max_word_len-min_word_len+1):
wordList.append(long_word[:-cut_length])
total_words += 1
if total_words >= num_words:
break
return wordList
if __name__ == '__main__':
# Sample word list
wordList = gen_word_list(1.5 * 10**5)
# Configs
try:
n_cores = int(sys.argv[-1] or 7)
except ValueError:
n_cores = 7
# Repetitions to do in order to estimate the runtime of a single run
num_repeats = 20
real_num_repeats = n_cores * num_repeats
# Creating Trie
root = TrieNode()
# Adding words
for word in wordList:
root.insertString(word, root)
# Extending trie to use it on multiple cores at once
multiroot = [root] * n_cores
# Measure time
print('Single process ...')
t_0 = time.time()
for i in range(real_num_repeats):
r = []
root.findSubStrings('he', root, r)
single_proc_time = (time.time()-t_0)
print(single_proc_time/real_num_repeats)
# using multicore to speed up the process
man = Manager()
# Loop to test the multicore Solution
# (Less repetitions are done to compare the timings to the single-core solution)
print('\nMultiprocess ...')
t_00 = time.time()
p_init_time = 0
procs_append_time = 0
p_start_time = 0
for i in range(num_repeats):
# Create Share-able list
res = man.list()
procs = []
for i in range(n_cores):
t_0 = time.time()
p = Process(target=multiroot[i].findSubStrings, args=('a', multiroot[i], res))
t_1 = time.time()
p_init_time += t_1 - t_0
procs.append(p)
t_2 = time.time()
procs_append_time += t_2 - t_1
p.start()
p_start_time += time.time() - t_2
for p in procs:
p.join()
multi_proc_time = time.time() - t_00
print(multi_proc_time / real_num_repeats)
init_overhead = p_init_time / single_proc_time
append_overhead = procs_append_time / single_proc_time
start_overhead = p_start_time / single_proc_time
total_overhead = (multi_proc_time - single_proc_time) / single_proc_time
print(f"Process(...) overhead: {init_overhead:.1%}")
print(f"procs.append(p) overhead: {append_overhead:.1%}")
print(f"p.start() overhead: {start_overhead:.1%}")
print(f"Total overhead: {total_overhead:.1%}")
Single process ...
0.007229958261762347
Multiprocess ...
0.7615800397736686
Process(...) overhead: 0.9%
procs.append(p) overhead: 0.0%
p.start() overhead: 8.2%
Total overhead: 10573.8%
答案 0 :(得分:3)
要考虑的事情很多,Multiprocessing > Programming guidelines中已经描述了大多数事情。最重要的是要记住,您实际上正在使用多个进程,因此有3种(或4种)处理变量的方式:
ctypes共享状态变量上的同步包装器(例如
multiprocessing.Value
)。实际变量始终是“一个对象”
内存,默认情况下,包装器使用“锁定”来设置/获取实数
值。
代理(例如Manager().list()
)。这些变量类似于共享状态变量,但是放置在特殊的“服务器进程”中,并且对它们的所有操作实际上都在管理器进程和活动进程之间发送腌制的值:
results.append(x)
腌制x
并将其从管理器进程发送到进行此调用的活动进程,
然后就没腌了
对results
的任何其他访问(例如len(results)
,结果迭代)都涉及相同的酸洗/发送/取消酸洗过程。
因此对于通用变量,代理通常比任何其他方法慢得多,并且在许多情况下使用管理器 与单进程运行相比,“本地”并行化将提供较差的性能。 但是经理服务器可以远程使用,因此当您希望并行化工作时使用它们是合理的 使用分布在多台机器上的工人
在子流程创建期间可用的对象。对于“ fork”启动方法,子过程创建期间所有可用的对象仍然可用并且“不共享”,因此更改它们只能“局部地为子过程”更改。但是在更改它们之前,每个进程实际上都“共享”了每个此类对象的内存,所以:
如果将它们用作“只读”,则不会复制或“通信”任何内容。
如果更改了它们,则将它们复制到子流程中,并且副本也将被更改。这称为写时复制或COW。 请注意,重新引用对象,例如分配变量 引用它,或将其附加到列表中会增加对象的ref_count,即 被认为是“变化”。
行为也可能会根据“启动方法”而有所不同:例如对于“ spawn” /“ forkserver”方法,可更改的全局变量实际上不是由子进程看到的“相同对象”值,可能与父进程中的不一样。
因此multiroot[i]
(在Process(target=..., args=(..., multiroot[i], ...))
中使用)的初始值是共享的,但是:
如果您不使用“ fork”启动方法(默认情况下Windows不使用它),则对于每个子进程,至少对所有arg进行一次酸洗。因此,如果start
很大,multiroot[i].children
可能会花费很长时间。
即使您使用的是fork:最初的multiroot[i]
似乎是共享的,而不是被复制的,但是我不确定会发生什么
如果在findSubStrings
方法内部分配了变量(例如currentNode = ...
),则可能是写时复制(COW),因此TrieNode
的整个实例都将被复制。
可以采取什么措施来改善这种情况:
如果您使用的是fork
启动方法,请确保“数据库”对象(TrieNode
实例)是真实的
readonly和do n't事件中包含带有变量分配的方法。例如,您可以将findSubStrings
移到另一个类,并确保在启动子过程之前调用所有instance.insertString
。
您正在将man.list()
实例用作results
的{{1}}参数。这意味着对于每个子流程
创建了一个不同的“包装器”,并且所有findSubStrings
操作都在酸洗results.append(prefix)
,然后将其发送
到服务器进程。如果您使用prefix
的进程数量有限,那么这没什么大不了的。如果您正在产卵
大量的子流程,则可能会影响性能。而且我认为默认情况下,它们都使用“锁定”,因此并发追加迁移相对较慢。如果Pool
中的项目顺序无关紧要(我对前缀树没有经验,也不记得其背后的理论),那么您可以完全避免与并发相关的任何开销
results
:
results.append
方法内创建新的results
列表。完全不要使用findSubStrings
。res = man.list()
返回的每个结果对象;
得到结果; “合并它们”。在findSubStrings中使用pool.apply_async()
将导致currentNode = root
的COW。这就是为什么弱引用(root
)可以带来一些额外的好处。
currentNodeRef = weakref.ref(root)
注意:
import string
import sys
import time
import weakref
from copy import deepcopy
from multiprocessing import Pool
from itertools import combinations_with_replacement
class TrieNode:
def __init__(self):
self.isString = False
self.children = {}
def insertString(self, word, root):
current_node = root
for char in word:
if char not in current_node.children:
current_node.children[char] = TrieNode()
current_node = current_node.children[char]
current_node.isString = True
# findStrings: not a method of TrieNode anymore, and works with reference to node.
def findStrings(prefix, node_ref, results):
# Hänge das Ergebnis an, wenn ein Ende gefunden wurde
if node_ref().isString:
results.append(prefix)
for char in node_ref().children:
findStrings(prefix + char, weakref.ref(node_ref().children[char]), results)
# findSubStrings: not a method of TrieNode anymore, and works with reference to node.
def findSubStrings(start_prefix, node_ref, results=None):
if results is None:
results = []
current_node_ref = node_ref
for char in start_prefix:
# Beende Schleife auf fehlende Prefixes oder deren Kinder
if char not in current_node_ref().children:
break
# Wechsle zu Kindern in anderem Falle
else:
current_node_ref = weakref.ref(current_node_ref().children[char])
# Verwende findStrings Rekursiv zum auffinden von End-Knoten
findStrings(start_prefix, current_node_ref, results)
return results
def gen_word_list(num_words, min_word_len=4, max_word_len=10):
wordList = []
total_words = 0
for long_word in combinations_with_replacement(string.ascii_lowercase, max_word_len):
wordList.append(long_word)
total_words += 1
if total_words >= num_words:
break
for cut_length in range(1, max_word_len-min_word_len+1):
wordList.append(long_word[:-cut_length])
total_words += 1
if total_words >= num_words:
break
return wordList
if __name__ == '__main__':
# Sample word list
wordList = gen_word_list(1.5 * 10**5)
# Configs
try:
n_cores = int(sys.argv[-1] or 7)
except ValueError:
n_cores = 7
# Repetitions to do in order to estimate the runtime of a single run
real_num_repeats = 420
simulated_num_repeats = real_num_repeats // n_cores
# Creating Trie
root = TrieNode()
# Adding words
for word in wordList:
root.insertString(word, root)
# Create tries for subprocesses:
multiroot = [deepcopy(root) for _ in range(n_cores)]
# NOTE: actually all subprocesses can use the same `root`, but let's copy them to simulate
# that we are using different tries when splitting job to sub-jobs
# localFindSubStrings: defined after `multiroot`, so `multiroot` can be used as "shared" variable
def localFindSubStrings(start_prefix, root_index=None, results=None):
if root_index is None:
root_ref = weakref.ref(root)
else:
root_ref = weakref.ref(multiroot[root_index])
return findSubStrings(start_prefix, root_ref, results)
# Measure time
print('Single process ...')
single_proc_num_results = None
t_0 = time.time()
for i in range(real_num_repeats):
iteration_results = localFindSubStrings('help', )
if single_proc_num_results is None:
single_proc_num_results = len(iteration_results)
single_proc_time = (time.time()-t_0)
print(single_proc_time/real_num_repeats)
# Loop to test the multicore Solution
# (Less repetitions are done to compare the timings to the single-core solution)
print('\nMultiprocess ...')
p_init_time = 0
apply_async_time = 0
results_join_time = 0
# Should processes be joined between repeats (simulate single job on multiple cores) or not (simulate multiple jobs running simultaneously)
PARALLEL_REPEATS = True
if PARALLEL_REPEATS:
t_0 = time.time()
pool = Pool(processes=n_cores)
t_1 = time.time()
p_init_time += t_1 - t_0
async_results = []
final_results = []
t_00 = time.time()
for repeat_num in range(simulated_num_repeats):
final_result = []
final_results.append(final_result)
if not PARALLEL_REPEATS:
t_0 = time.time()
pool = Pool(processes=n_cores)
t_1 = time.time()
p_init_time += t_1 - t_0
async_results = []
else:
t_1 = time.time()
async_results.append(
(
final_result,
pool.starmap_async(
localFindSubStrings,
[('help', core_num) for core_num in range(n_cores)],
)
)
)
t_2 = time.time()
apply_async_time += t_2 - t_1
if not PARALLEL_REPEATS:
for _, a_res in async_results:
for result_part in a_res.get():
t_3 = time.time()
final_result.extend(result_part)
results_join_time += time.time() - t_3
pool.close()
pool.join()
if PARALLEL_REPEATS:
for final_result, a_res in async_results:
for result_part in a_res.get():
t_3 = time.time()
final_result.extend(result_part)
results_join_time += time.time() - t_3
pool.close()
pool.join()
multi_proc_time = time.time() - t_00
# Work is not really parallelized, instead it's just 'duplicated' over cores,
# and so we divide using `real_num_repeats` (not `simulated_num_repeats`)
print(multi_proc_time / real_num_repeats)
init_overhead = p_init_time / single_proc_time
apply_async_overhead = apply_async_time / single_proc_time
results_join_percent = results_join_time / single_proc_time
total_overhead = (multi_proc_time - single_proc_time) / single_proc_time
print(f"Pool(...) overhead: {init_overhead:.1%}")
print(f"pool.starmap_async(...) overhead: {apply_async_overhead:.1%}")
print(f"Results join time percent: {results_join_percent:.1%}")
print(f"Total overhead: {total_overhead:.1%}")
for iteration_results in final_results:
num_results = len(iteration_results) / n_cores
if num_results != single_proc_num_results:
raise AssertionError(f'length of results should not change! {num_results} != {single_proc_num_results}')
模拟多个作业的运行(例如,每个作业应以不同的前缀启动,但在示例中,我使用相同的前缀来使每次运行具有一致的“负载”),并且每个作业均为“并行化”。 PARALLEL_REPEATS=True
模拟单个作业的运行
在所有内核上并行化,并且比单进程慢
解决方案。PARALLEL_REPEATS=False
超过1次。示例输出:
apply_async
答案 1 :(得分:0)
首先,我要感谢所有参与其中的人,因为每个答案都为解决方案做出了贡献。
正如第一批评论所指出的,每次创建一个新进程都会导致python将所需的数据转移到该进程中。这可能需要花费几秒钟的时间,并且会导致不希望的延迟。
为我带来了最终解决方案的是,在程序启动期间,一次使用多处理库的Process类创建了一次进程(每个内核一个)。
然后您可以使用同一模块的Pipe类与流程进行通信。
我发现这里的乒乓示例确实有帮助:https://www.youtube.com/watch?v=s1SkCYMnfbY&t=900s
它仍然不是最佳选择,因为试图同时与该进程通信的多个管道会导致该进程崩溃。
但是,我应该能够使用队列解决此问题。如果有人对解决方案感兴趣,请随时询问。