Eu estou experimentando com fluxos paralelos em Java e por isso eu tenho o seguinte código para o cálculo de número de primos antes n
.
Basicamente eu estou tendo 2 métodos
calNumberOfPrimes(long n)
- 4 variantes diferentesisPrime(long n)
- 2 variantes diferentes
Na verdade, eu estou tendo 2 variantes diferentes de cada um o método acima, uma variante que usa fluxos paralelos e outras variantes que não usam fluxos paralelos.
// itself uses parallel stream and calls parallel variant isPrime
private static long calNumberOfPrimesPP(long n) {
return LongStream
.rangeClosed(2, n)
.parallel()
.filter(i -> isPrimeParallel(i))
.count();
}
// itself uses parallel stream and calls non-parallel variant isPrime
private static long calNumberOfPrimesPNP(long n) {
return LongStream
.rangeClosed(2, n)
.parallel()
.filter(i -> isPrimeNonParallel(i))
.count();
}
// itself uses non-parallel stream and calls parallel variant isPrime
private static long calNumberOfPrimesNPP(long n) {
return LongStream
.rangeClosed(2, n)
.filter(i -> isPrimeParallel(i))
.count();
}
// itself uses non-parallel stream and calls non-parallel variant isPrime
private static long calNumberOfPrimesNPNP(long n) {
return LongStream
.rangeClosed(2, n)
.filter(i -> isPrimeNonParallel(i))
.count();
}
// uses parallel stream
private static boolean isPrimeParallel(long n) {
return LongStream
.rangeClosed(2, (long) Math.sqrt(n))
.parallel()
.noneMatch(i -> n % i == 0);
}
// uses non-parallel stream
private static boolean isPrimeNonParallel(long n) {
return LongStream
.rangeClosed(2, (long) Math.sqrt(n))
.noneMatch(i -> n % i == 0);
}
Estou tentando razão para fora que, entre calNumberOfPrimesPP
, calNumberOfPrimesPNP
, calNumberOfPrimesNPP
e calNumberOfPrimesNPNP
é o melhor em termos de uso adequado dos fluxos paralelos com eficiência e por que ele é o melhor.
Tentei tempo todos estes 4 métodos em 50 vezes e tomou a média usando o seguinte código:
public static void main(String[] args) throws Exception {
int iterations = 50;
int n = 1000000;
double pp, pnp, npp, npnp;
pp = pnp = npp = npnp = 0;
for (int i = 0; i < iterations; i++) {
Callable<Long> runner1 = () -> calNumberOfPrimesPP(n);
Callable<Long> runner2 = () -> calNumberOfPrimesPNP(n);
Callable<Long> runner3 = () -> calNumberOfPrimesNPP(n);
Callable<Long> runner4 = () -> calNumberOfPrimesNPNP(n);
pp += TimeIt.timeIt(runner1);
pnp += TimeIt.timeIt(runner2);
npp += TimeIt.timeIt(runner3);
npnp += TimeIt.timeIt(runner4);
}
System.out.println("___________final results___________");
System.out.println("avg PP = " + pp / iterations);
System.out.println("avg PNP = " + pnp / iterations);
System.out.println("avg NPP = " + npp / iterations);
System.out.println("avg NPNP = " + npnp / iterations);
}
TimeIt.timeIt
simplesmente retorna o tempo de execução em mili-segundos. Eu tenho o seguinte resultado:
___________final results___________
avg PP = 2364.51336366
avg PNP = 265.27284506
avg NPP = 11424.194316620002
avg NPNP = 1138.15516624
Agora eu estou tentando raciocinar sobre os tempos de execução acima:
- A
PP
variante não é tão rápido comoPNP
variante, porque todos os fluxos paralelos usar fork-join comum pool de threads e se enviar uma tarefa de longa duração, estamos bloqueando efetivamente os tópicos na piscina. - Mas o argumento acima também deve trabalhar para
NPP
variante e assim aNPP
variante também deve ser aproximadamente tão rápido quanto aPNP
variante. (Mas este não é o caso,NPP
variante é o pior em termos de tempo necessário). Alguém pode explicar a razão por trás disso?
Minhas perguntas:
- É o meu raciocínio correto para o pequeno tempo de execução da
PNP
variante? - Estou esquecendo de algo?
- Por
NPP
variante é a pior (em termos de tempo de execução)?
Como TimeIt
é medir o tempo:
class TimeIt {
private TimeIt() {
}
/**
* returns the time to execute the Callable in milliseconds
*/
public static <T> double timeIt(Callable<T> callable) throws Exception {
long start = System.nanoTime();
System.out.println(callable.call());
return (System.nanoTime() - start) / 1.0e6;
}
}
PS: Eu entendo que este não é o melhor método para contar o número de primos. Crivo de Eratóstenes e outros métodos mais sofisticados existe para fazer isso. Mas por este exemplo, eu só quero entender o comportamento dos fluxos paralelos e quando usá-los.
Eu acho que, é claro, porque NPP é tão lento.
Organizar os números resultantes de uma tabela:
| _P | _NP
-------+----------+---------
P_ | 2364 | 265
-------+----------+---------
NP_ | 11424 | 1138
-------+----------+---------
Então você vê que é sempre mais rápido quando o fluxo externo é paralelo. Isso ocorre porque há muito trabalho a ser feito no córrego. Assim, a sobrecarga adicional para o tratamento do fluxo paralelo é baixo quando comparado com o trabalho a ser feito.
Você também ver que é sempre mais rápido quando o fluxo interior não é paralelo. isPrimeNonParallel
é mais rápido do que isPrimeParallel
. Isso ocorre porque não há muito trabalho a ser feito no córrego. Na maioria dos casos, é evidente depois de alguns passos que o número não é primo. Metade dos números são ainda (apenas uma etapa). A sobrecarga adicional para o tratamento do fluxo paralelo é elevado em comparação com o trabalho a ser feito.