O desempenho do multiprocessamento Python melhora apenas com a raiz quadrada do número de núcleos usados

Estou tentando implementar o multiprocessamento em Python (Windows Server 2012) e estou tendo problemas para alcançar o grau de melhoria de desempenho que eu espero. Em particular, para um conjunto de tarefas quase inteiramente independentes,Eu esperaria uma melhoria linear com núcleos adicionais.

Entendo que - especialmente no Windows - há uma sobrecarga envolvida na abertura de novos processos[1], e que muitas peculiaridades do código subjacente podem atrapalhar uma tendência limpa. Mas, em teoria, a tendência ainda deve estar próxima de linear para uma tarefa totalmente paralela.[2]; ou talvez logística, se eu estivesse lidando com uma tarefa parcialmente serial[3].

No entanto, quando executo o multiprocessamento.Pool em uma função de teste de verificação primária (código abaixo), obtenho um relacionamento de raiz quadrada quase perfeito atéN_cores=36 (o número de núcleos físicos no meu servidor) antes que o desempenho esperado seja atingido quando eu entrar nos núcleos lógicos adicionais.

Aqui é uma plotagem dos meus resultados dos testes de desempenho:
("Desempenho Normalizado" é[ um tempo de execução com1 CPU-core] dividido por[ um tempo de execução comN Núcleos de CPU] )

É normal ter essa diminuição dramática de retornos com o multiprocessamento? Ou estou perdendo algo com minha implementação?

import numpy as np
from multiprocessing import Pool, cpu_count, Manager
import math as m
from functools import partial
from time import time

def check_prime(num):

    #Assert positive integer value
    if num!=m.floor(num) or num<1:
        print("Input must be a positive integer")
        return None

    #Check divisibility for all possible factors
    prime = True
    for i in range(2,num):
        if num%i==0: prime=False
    return prime

def cp_worker(num, L):
    prime = check_prime(num)
    L.append((num, prime))


def mp_primes(omag, mp=cpu_count()):
    with Manager() as manager:
        np.random.seed(0)
        numlist = np.random.randint(10**omag, 10**(omag+1), 100)

        L = manager.list()
        cp_worker_ptl = partial(cp_worker, L=L)

        try:
            pool = Pool(processes=mp)   
            list(pool.imap(cp_worker_ptl, numlist))
        except Exception as e:
            print(e)
        finally:
            pool.close() # no more tasks
            pool.join()

        return L


if __name__ == '__main__':
    rt = []
    for i in range(cpu_count()):
        t0 = time()
        mp_result = mp_primes(6, mp=i+1)
        t1 = time()
        rt.append(t1-t0)
        print("Using %i core(s), run time is %.2fs" % (i+1, rt[-1]))

Nota: Estou ciente de que, para esta tarefa, seria provavelmente mais eficiente implementar váriosrosqueamento, mas o script real para o qual este é um analógico simplificado é incompatível com o multithreading do Python devido ao GIL.

questionAnswers(1)

yourAnswerToTheQuestion