并行化第 n 个阶乘 python 程序的更好方法?

Better way to parallelize my nth factorial python program?

我有这个 python 代码来计算连续 n 个“1”的第 n 个阶乘。我已经能够很好地优化它,包括使用多处理模块在所有内核上将其调整为 运行。但是我注意到第 7 个进程(这是我从上到下的值的下限)比其余线程快得多。当 n=11 时,线程 0-6 平均耗时 32 秒,而线程 7 仅耗时 12 秒。我本以为数字本身会有所不同,但我不希望立即出现如此明显差异的墙。

我的代码中是否遗漏了导致这堵大墙的计算?我已经验证了输出并且每个段的长度几乎相同(线程 7 通过几十次计算略长,但在宏伟的计划中这没什么,线程 7 是最短的 运行ning 无论如何)

是否有更好的并行化方法以提高效率?使线程不完全相同会有帮助吗?

编辑:添加python版本信息

Python 3.8.5(tags/v3.8.5:580fbb0,2020 年 7 月 20 日,15:57:54)[MSC v.1924 64 位 (AMD64)] on win32

(我对n=11做了25次测试,都和这个很相似运行)

import multiprocessing
import argparse
from datetime import datetime
from math import log10

parser = argparse.ArgumentParser(
    formatter_class=argparse.HelpFormatter,
    description="Calcs n factorial",
    usage=""
)

parser.add_argument("-n", "--number", type=int, default=2)

args = parser.parse_args()

def getlog(send_end, i, threads, num, n, inc):
    begin = datetime.now()
    start = num-inc*i
    end = num-inc*(i+1) if i < threads-1 else 0
    output = sum(map(log10, range(start, end, -n)))
    send_end.send(output)
    final = datetime.now()
    duration = final-begin
    print("{},{},{},{}".format(i, duration, start, end))

def main():
    n = args.number
    num = int('1'*n)
    threads = multiprocessing.cpu_count() if num/multiprocessing.cpu_count() > multiprocessing.cpu_count() else 1
    inc = int(num/threads)
    inc -= inc%n
    jobs = []
    pipe_list = []
    for i in range(threads):
        recv_end, send_end = multiprocessing.Pipe(False)
        p = multiprocessing.Process(target=getlog, args=(send_end, i, threads, num, n, inc))
        jobs.append(p)
        pipe_list.append(recv_end)
        p.start()
    for proc in jobs:
        proc.join()
    e = sum([output.recv() for output in pipe_list])

    print('%.2fe%d' % (10**(e % 1), e // 1))
    
if __name__ == '__main__':
    start = datetime.now()
    main()
    end = datetime.now()
    print(end-start)

迭代一百万个不同量级的数字的次数:

from timeit import repeat
from collections import deque

for e in range(26, 36):
    n = 2**e
    t = min(repeat(lambda: deque(range(n, n+10**6), 0), number=1))
    print(e, t)

我在 32 位 Python 在 64 位 Windows 上的输出,请注意从 230 到 231:

26 0.020830399999999916
27 0.020713199999999987
28 0.02067260000000004
29 0.021565000000000056
30 0.021966000000000152
31 0.16404839999999998
32 0.16630840000000013
33 0.16394810000000026
34 0.16302989999999973
35 0.1655395999999998

范围内的映射 log10 仍然显示大致相同的(绝对)增加:

26 0.14502039999999994
27 0.1435571
28 0.14378349999999962
29 0.14398270000000002
30 0.14687919999999988
31 0.29700239999999933
32 0.29499730000000035
33 0.2949491999999996
34 0.2964432000000006
35 0.2918921999999995

代码:

from timeit import repeat
from collections import deque
from math import log10

for e in range(26, 36):
    n = 2**e
    t = min(repeat(lambda: deque(map(log10, range(n, n+10**6)), 0), number=1))
    print(e, t)

并且您在线程 7 中的数字都是快速量级,而 most/all 其他线程中的数字是慢速量级。

你可以改变你的范围,让它们都经过所有星等。更简单的例子:使用范围 range(0, 10)range(10, 20),而不是范围 range(0, 20, 2)range(1, 20, 2).

顺便说一句,当从 230 到 2[=44 时,我看到 64 位 Python 在 64 位 Windows 上有类似的增长=]31。但是在 Linux 上的 64 位 Python 上,从 230 到 231 时我没有看到任何增加,但是从 262 到 263.

更新:

以上striked-through段不正确。正如 所示,并不是因为“数字”很慢(我曾想过),而是因为有两个完全独立的 range 实现。并且只有您的线程 7 使用快速线程(用于小数字的线程)。因此,我上面提出的让所有 threads/ranges 经历所有量级的建议实际上是 counter-productive。它不会让慢的更快,它只会让快的和其他的一样慢。无赖。

所以备选建议:不要像您那样给每个线程一个范围,而是给每个线程一部分 long 范围和一部分非 long 范围。这应该使所有线程同样快,并减少总时间。但是影响会很小,n越大影响越小,我怀疑这是否值得复杂化。

range 如果需要处理超出 C long 范围的值,则使用较慢的实现 - 请参阅 source.

您在 Windows,其中 C long 是 32 位的(甚至在 64 位 Python 版本上)。过程 7 是唯一一个范围元素在 C long.

范围内的过程