Search code examples
pythonpython-3.xpython-multiprocessing

Multiprocessing only utilizing a single core


I'm trying to create a FBM texture using the module perlin-noise, but it takes a very long time to execute. I've implemented multiprocessing, only to find that the program was still running off a single core. I've tried looking for other people with the same problem, but most threads were 7+ years old and / or involved problems and solutions related to different OSs.

My OS is Windows 8.1, I have a quad-core CPU, and I'm running Python 3.9.2

Here is the program:

from perlin_noise import PerlinNoise
from multiprocessing import Pool

def world(n):
    noise1 = PerlinNoise(octaves=3, seed=1)
    noise2 = PerlinNoise(octaves=6, seed=1)
    noise3 = PerlinNoise(octaves=12, seed=1)
    noise4 = PerlinNoise(octaves=24, seed=1)
    noise5 = PerlinNoise(octaves=48, seed=1)
        
    world = []
    for i in range(n):
        row = []
        for j in range(n):
            noise_val =  noise1([i/n, j/n])
            noise_val += 0.5 * noise2([i/n, j/n])
            noise_val += 0.25 * noise3([i/n, j/n])
            noise_val += 0.125 * noise4([i/n, j/n])
            noise_val += 0.0625 * noise5([i/n, j/n])
            row.append(noise_val)
        world.append(row)
    
def main():
    start = time.time()
    nums = [128]
    p = Pool()
    p.map(world, nums)
    p.close()
    p.join()
    end = time.time()
    print(end - start)
    
if __name__ == '__main__':
    import time
    from distributed import Client
    client = Client()
    main()

So, what's going on? Have I made a mistake in thinking that multiprocessing could work on these for-loops?

Thanks!


Solution

  • Reason why it only use one Process is simple. You only passed 1-length list in Pool.map.

    What Pool(n).map(function, iterable) does is, applying provided funtion to each element of provided iterable(in this case, list) with n number of worker processes.

    Since you only have 128 in nums it's only creating one task thus no other processes are ever used.

    Proper usage would look like this:

    from multiprocessing import pool
    
    
    def work(val):
        return hash(val ** 10000000)
    
    
    if __name__ == '__main__':
        p = pool.Pool(8)
    
        with p:
            output = p.map(work, [i for i in range(1, 31)])
    
        print(output)
    
    

    This example will have 8 processes, so utilizing 8 logical cores. Since we gave it numbers from 1 to 30, p.map will apply function work to each of those numbers using 8 processes, so it can run up to 8 functions simultaneously.

    When we run it, we can see it's effect.

    enter image description here

    Of course it's using more processes to communicates between process, etc - I admit that I don't know underlying details on it.


    Side note, to make your work more efficient, you should refrain yourself from appending as much as possible.

    Check this simple example appending a lot of time and measuring time it took.

    >>> import timeit
    >>> def appending():
    ...     output = []
    ...     for i in range(1000000):
    ...         output.append(i)
    ...     return output
    ... 
    >>> def list_comp():
    ...     return [i for i in range(1000000)]
    
    >>> print(f"{timeit.timeit(appending, number=100):.2}")
    8.1
    
    >>> print(f"{timeit.timeit(list_comp, number=100):.2}")
    5.2
    

    As you can see appending is pretty much more slower than List comprehension - but this doesn't mean not to use list.append - just don't use it excessively.