ABZ*_*TER 2 python parallel-processing multithreading numba
当我使用 numba 中的 njit 并行运行该程序时,我注意到使用多个线程并没有什么区别。事实上,从 1-5 个线程开始,时间会更快(这是预期的),但之后时间会变慢。为什么会发生这种情况?
\nfrom numba import njit,prange,set_num_threads,get_num_threads\nimport numpy as np\n@njit(parallel=True)\ndef test(x,y):\n z=np.empty((x.shape[0],x.shape[0]),dtype=np.float64)\n for i in prange(x.shape[0]):\n for j in range(x.shape[0]):\n z[i,j]=x[i,j]*y[i,j]\n return z\nRun Code Online (Sandbox Code Playgroud)\nx=np.random.rand(10000,10000)\ny=np.random.rand(10000,10000)\nfor i in range(16): \n set_num_threads(i+1)\n print("Number of threads :",get_num_threads())\n %timeit -r 1 -n 10 test(x,y)\nRun Code Online (Sandbox Code Playgroud)\nNumber of threads : 1\n234 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 2\n178 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 3\n168 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 4\n161 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 5\n148 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 6\n152 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 7\n152 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 8\n153 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 9\n154 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 10\n156 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 11\n158 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 12\n157 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 13\n158 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 14\n160 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 15\n160 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nNumber of threads : 16\n161 ms \xc2\xb1 0 ns per loop (mean \xc2\xb1 std. dev. of 1 run, 10 loops each)\nRun Code Online (Sandbox Code Playgroud)\n我在 Jupyter Notebook (anaconda) 的 8 核 16 线程 CPU 中对此进行了测试。
\n该代码受内存限制,因此 RAM 在只有少数核心的情况下就饱和了。
事实上,z[i,j]=x[i,j]*y[i,j]由于x86-64 处理器上的写入分配缓存策略,会导致两次 8 字节的内存加载、一次 8 字节的存储和额外的 8 字节加载(在这种情况下必须读取写入的缓存行)。这意味着每次循环迭代加载/存储 32 个字节,而只需要执行 1 次乘法。现代主流 (x86-64) 处理器可以执行 2x4 双精度 FP 乘法/周期,并在 3-5 GHz 下运行(事实上,英特尔服务器处理器可以执行 2x8 DP FP 乘法/周期)。而好的主流PC只能达到40-60 GiB/s,高性能服务器只能达到200-350 GiB/s。
在 Numba 中没有办法加速像这样的内存绑定代码。C/C++ 代码可以通过避免写入分配来稍微改进这一点(最多快 1.33 倍)。最好的解决方案是如果可能的话在较小的块上进行操作并合并计算步骤,以便每个步骤应用更多的 FP 操作。
实际上,众所周知,与处理器的计算能力相比,RAM 的速度增长缓慢。这个问题几十年前就已经被发现,而且随着时间的推移,两者之间的差距仍然越来越大。这个问题被称为“内存墙”。未来情况不会更好(至少不太可能是这种情况)。