lunes 9 de febrero de 2009

Paraprimes


En el artículo anterior se mostraba el cálculo de números primos con la librería GMP y en el anterior a éste, el cálculo con SQL y Perl.

Ahora vamos un paso más allá, distribuyendo el cálculo en procesos paralelos. Para ello, hay múltiples soluciones, como por ejemplo:

Sin embargo, la opción más sencilla probablemente sea OpenMP, un estándar abierto que ya se encuentra integrado en el compilador GCC.

El siguiente código, que se compila con "gcc -fopenmp -o hola hola.c", es un ejemplo muy sencillo del uso de OpenMP:

hola.c
#include <omp.h>
#include <stdio.h>

// @author José Cotrino (http://www.cotrino.com)
int main() {
    #pragma omp parallel
printf("- Hola caracola desde el hilo %d, hay %d hilos\n", omp_get_thread_num(), omp_get_num_threads());

const int N = 100000;
const int MAX_THREADS = 20;
int i, a[N], p[MAX_THREADS];
for (i=0;i<MAX_THREADS;i++) p[i]= 0;
printf("\n- Calculando un bucle 'for' de %d iteraciones en paralelo: \n + cada iteración puede ser asignada teóricamente a un procesador\n + el array sobre el que se calcula está en memoria compartida\n",N);
    #pragma omp parallel for
for (i=0;i<N;i++) {
a[i]= 2*i;
p[omp_get_thread_num()] ++;
}
printf("\n- Yatá!\n");
for (i=0;i<MAX_THREADS;i++) {
if (p[i] != 0)
printf(" + Hilo %d se ha ejecutado %d veces\n", i, p[i]);
}
}


En un Intel Core 2 Duo obtenemos la siguiente salida:

- Hola caracola desde el hilo 1, hay 2 hilos
- Hola caracola desde el hilo 0, hay 2 hilos
- Calculando un bucle 'for' de 100000 iteraciones en paralelo:
+ cada iteración puede ser asignada teóricamente a un procesador
+ el array sobre el que se calcula está en memoria compartida
- Yatá!
+ Hilo 0 se ha ejecutado 50000 veces
+ Hilo 1 se ha ejecutado 50000 veces


Volviendo al código que teníamos para calcular primos con GMP, lo que haremos es simplemente dividir todo el rango en varios trozos, de forma que cada procesador calcule los números primos dentro de un subrango. Por ejemplo, si tenemos 2 núcleos y queremos calcular todos los primos entre 0 y 1000, en un procesador calculamos entre 0 y 500, y en el otro entre 501 y 1000. Esto es posible porque el test de primalidad que utiliza GMP no necesita conocer los primos anteriores.

El código queda así:

paraprimes.cpp
#include <stdio.h>
#include <time.h>
#include <gmp.h>
#include <omp.h>

// @author José Cotrino (http://www.cotrino.com)
void calculatePrimes ( mpz_t N_max ) {
time_t start,end;
double elapsed;
mpz_t N_primes, N_range;
mpz_init(N_primes);
mpz_init(N_range);
mpz_set_ui(N_primes, 0);
mpz_set(N_range, N_max);

time(&start);

// calculate N/threads
int N_cores = omp_get_num_procs();
mpz_cdiv_q_ui(N_range, N_range, N_cores);


// each thread calculates the primes between N/t*i and N/t*(i+1), where:
// - N is the total range where to search for primes
// - t is the amount of available processors
// - i is the number of the current thread/processor (0, 1, 2, 3...)
    #pragma omp parallel shared(N_range, N_primes)
{
int thread = omp_get_thread_num();
mpz_t prime, N_max_thread, N_primes_thread;
mpz_init(prime);
mpz_init(N_max_thread);
mpz_init(N_primes_thread);
mpz_set(prime, N_range);
mpz_mul_ui(prime, prime, thread);
mpz_add_ui(prime, prime, 1);
mpz_set(N_max_thread, N_range);
mpz_mul_ui(N_max_thread, N_max_thread, thread+1);
mpz_set_ui(N_primes_thread, 0);

//printf("Thread %d searching in range %s, %s\n", thread, mpz_get_str(NULL,10,prime), mpz_get_str(NULL,10,N_max_thread));
while( mpz_cmp(prime, N_max_thread) < 0 ) {
mpz_nextprime(prime, prime);
//printf("%s, ", mpz_get_str(NULL,10,prime));
mpz_add_ui(N_primes_thread, N_primes_thread, 1);
}
// skip the last prime, which is over the range
mpz_sub_ui(N_primes_thread, N_primes_thread, 1);
// add the primes found by this thread to the whole amount
mpz_add(N_primes, N_primes, N_primes_thread);
}

time(&end);
elapsed = difftime(end, start);

printf("%s primes found below %s in %.0f seconds\n",
mpz_get_str(NULL,10,N_primes),
mpz_get_str(NULL,10,N_max),
elapsed);
//printf("%s %f\n",mpz_get_str(NULL,10,N_max),elapsed);
}

int main() {
int i;
mpz_t N_max;
mpz_init(N_max);
mpz_set_ui(N_max, 100);

printf("%d cores available\n", omp_get_num_procs());

for(i=0; i<8; i++) {
calculatePrimes(N_max);
mpz_mul_ui(N_max, N_max, 10);
}
}


Y se compila con:
g++ -lgmp -fopenmp paraprimes.cpp -o paraprimes

Los resultados en un Intel Core 2 Duo T5750 son impresionantes:


Máximo númeroPrimosDuración
100250s
1.0001680s
10.0001.2290s
100.0009.5920s
1.000.00078.4980s
10.000.000664.5797s
100.000.0005.761.45566s (~1.1min)
1.000.000.00050.847.534703s (~11.7min)


Comparamos de nuevo con las implementaciones anteriores para calcular los primos por debajo de 100 millones:
  • El algoritmo en C utilizando GMP sin paralelizar necesitaba 126 segundos. Con dos procesadores necesitamos 66 segundos, es decir, es 1.91 veces más rápido.

  • El algoritmo en Perl necesitaba 5.929 segundos, que comparados con los 66 segundos actuales nos da que este algoritmo en paralelo es 89.83 veces más rápido.


Teóricamente, con este algoritmo y si la ley de Amdahl lo permite, con un quad-core se podrían calcular todos los primos por debajo de 100 millones en apenas 35 segundos, con un octo-core en 17 segundos, y con un Tesla en ná y menos. Me mantengo a la espera de que alguien me regale uno para poder hacer la prueba.

Nos ponemos serios


Tras el experimento anterior, en el que se usaba SQL y Perl para calcular números primos, ahora probaremos un método algo más profesional y bastante más rápido.

Utilizaremos la librería GMP, GNU Multiple Precision, un conjunto de funciones para operar con números enteros y flotantes de cualquier tamaño, donde muchas de las operaciones están implementadas directamente en ensamblador optimizado para varios tipos de procesadores.

Esta librería incorpora la función int mpz_probab_prime_p (mpz_t n, int reps), una función que ejecuta el test de primalidad de Miller-Rabin sobre un número dado. Es decir, en este caso ni aplicamos una criba ni dividimos entre todos los primos anteriores, aunque no tendremos la absoluta certeza de que todos los números obtenidos sean primos.

Este código me ha servido de base para el siguiente:

primes.cpp
#include <stdio.h>
#include <time.h>
#include <gmp.h>

// @author José Cotrino (http://www.cotrino.com)
void calculatePrimes ( mpz_t N_max ) {
time_t start,end;
double elapsed;
mpz_t prime, N_primes;
mpz_init(prime);
mpz_init(N_primes);
mpz_set_ui(prime, 1);
mpz_set_ui(N_primes, 0);

time(&start);

while( mpz_cmp(prime, N_max) < 0 ) {
mpz_nextprime(prime, prime);
//printf("%s, ", mpz_get_str(NULL,10,prime));
mpz_add_ui(N_primes, N_primes, 1);
}
// skip the last prime, which is over the range
mpz_sub_ui(N_primes, N_primes, 1);

time(&end);
elapsed = difftime(end, start);

/*printf("%s primes found below %s in %f seconds (last one found over the limit was %s)\n",
        mpz_get_str(NULL,10,N_primes),
        mpz_get_str(NULL,10,N_max),
        elapsed,
        mpz_get_str(NULL,10,prime));*/
printf("%s %.0f\n",mpz_get_str(NULL,10,N_max),elapsed);
}

int main() {
int i;
mpz_t N_max;
mpz_init(N_max);
mpz_set_ui(N_max, 100);
for(i=0; i<8; i++) {
calculatePrimes(N_max);
mpz_mul_ui(N_max, N_max, 10);
}
}


Se compila en Linux con:
g++ -lgmp primes.cpp -o primes

Los resultados obtenidos son algo así:


Máximo númeroPrimosDuración
100250s
1.0001680s
10.0001.2290s
100.0009.5920s
1.000.00078.4981s
10.000.000664.57912s
100.000.0005.761.455126s (~2min)
1.000.000.00050.847.5341.275s (~21min)


En el algoritmo en Perl, calcular todos los número primos por debajo de 100 millones llevó 5.929 segundos (1 hora y 39 minutos). En este caso, gracias a GMP, se ha tardado 126 segundos (poco más de 2 minutos) para el mismo rango. Es decir, se consigue lo mismo 47 veces más rápido.

No está mal. Apretaremos aún más las tuercas.

lunes 2 de febrero de 2009

Calcular números primos con SQL


¿Cuántos números primos hay dentro de los primeros 100.000 números naturales? La respuesta a esta pregunta nunca me interesó y me sigue sin interesar, pero el método para obtener la respuesta tiene su gracia.

Al leer el libro de Simon Singh "The Code Book", me ha cautivado la parte en la que describe el descubrimiento del algoritmo de clave pública, que se utiliza hoy día en casi todas las comunicaciones cifradas a través de Internet. Por ejemplo, la seguridad del sistema RSA, basado en números primos, se centra en el esfuerzo que requiere factorizar un número muuuy grande (mayor que 10100).

Y buceando por la Wikipedia di con el artículo sobre la criba de Eratóstenes, en el que se describe el algoritmo más sencillo, aunque no el más eficiente, para calcular todos los primos por debajo de un número determinado. En este artículo hay código en varios lenguajes mayoritarios, pero se echa en falta la versión en SQL. Así que, señoras y señores, aquí está la implementación del algoritmo que nadie había pedido, utilizando la base de datos SQLite desde Java mediante JDBC (es necesario el driver, que se puede descargar aquí).

Primes.java

package com.cotrino.primes;
import java.sql.SQLException;
/**
* Prime number calculator using SQL
* @author José Cotrino (http://www.cotrino.com)
*/

public class Primes {
static long N = 0;
static long primes = 0;
static boolean fileDB = false;

public Primes() {
DB db;
if( fileDB )
db = new DB("primes.db");
else
db = new DB(":memory:");
db.createTables();
db.massiveInsert(N);
try {
db.executeUpdate("UPDATE tbl_primes SET isPrime=0 "+
"WHERE id IN "+
"(SELECT tbl_primes.id*B.id FROM tbl_primes "+
"INNER JOIN tbl_primes AS B "+
"ON tbl_primes.id>=B.id AND tbl_primes.id*B.id<="+N+" "+
"AND B.id>=2 AND B.id*B.id<="+N+" AND B.isPrime=1);");
} catch(SQLException e) {
System.err.println("Calculation failed! "+e);
}
primes = db.getLong("SELECT COUNT(id) FROM tbl_primes WHERE isPrime=1;");
db.close();
}

public static void main(String[] args) {
if( args.length < 2 ) {
System.err.println("Please specify the amount of numbers "+
"where to search for primes (e.g. 1000) and "+
"if the DB should be file- or memory-based (true/false).");
System.exit(1);
}
long time = System.currentTimeMillis();
N = Long.parseLong(args[0]);
fileDB = Boolean.parseBoolean(args[1]);
new Primes();
time = System.currentTimeMillis() - time;
long memory = Math.round(java.lang.Runtime.getRuntime().totalMemory()/1000000);
System.out.println("Numbers\t\tPrimes\t\tFileDB\t\tMemory\t\tDuration\n"+
N+"\t\t"+primes+"\t\t"+fileDB+"\t\t"+memory+"MB\t\t"+time+"ms");
}
}



DB.java

package com.cotrino.primes;
import java.sql.*;
/**
* SQLite database handler for prime number calculator
* @author José Cotrino (http://www.cotrino.com)
*/

public class DB {
private Connection conn;
private Statement stat;

public DB(String databaseName) {
try {
Class.forName("org.sqlite.JDBC");
conn = DriverManager.getConnection("jdbc:sqlite:"+databaseName);
stat = conn.createStatement();
} catch(SQLException e) {
} catch(ClassNotFoundException e) {
}
}

public void executeUpdate(String query) throws SQLException {
stat.executeUpdate(query);
}

public ResultSet executeQuery(String query) throws SQLException {
return stat.executeQuery(query);
}

public long getLong(String query) {
long value = 0;
try {
ResultSet rs = executeQuery(query);
while(rs.next()) { value = rs.getLong(1); }
rs.close();
} catch(SQLException e) {
System.err.println("Query failed: "+query);
}
return value;
}

public void massiveInsert(long N) {
try {
PreparedStatement prep = conn.prepareStatement(
"INSERT INTO tbl_primes values (?, ?);");
for (long i = 2; i <= N; i++) {
prep.setLong(1, i);
prep.setInt(2, 1);
prep.addBatch();
}
conn.setAutoCommit(false);
prep.executeBatch();
conn.setAutoCommit(true);
} catch(SQLException e) {
System.err.println("Value insertion failed!");
}
}

public void close() {
try {
conn.close();
} catch(SQLException e) {
}
}

public void createTables() {
try {
executeUpdate("DROP TABLE IF EXISTS tbl_primes;");
executeUpdate(
"CREATE TABLE tbl_primes "+
"(id INTEGER PRIMARY KEY, isPrime INTEGER);"
);
} catch(SQLException e) {
System.err.println("Table creation failed!");
}
}
}




Una vez compilado y empaquetado, se ejecuta por ejemplo con este comando:
java -jar Primes.jar 100000 false

Estos son los resultados que devuelve este programa en un portátil viejuno con un procesador Pentium M 1.6 GHz y 1 GB de memoria. Hay una diferencia apreciable entre utilizar la base de datos en memoria o en fichero, aunque como la parte fundamental del algoritmo se ejecuta con una sola consulta SQL, las operaciones tienen lugar en memoria.

Máximo numeroPrimosFichero/MemoriaUso de memoriaDuración
10025memoria5MB180ms
10025fichero5MB711ms
1.000168memoria5MB221ms
1.000168fichero5MB1042ms
10.0001229memoria5MB761ms
10.0001229fichero5MB1753ms
100.0009592memoria6MB15283ms (15 sec)
100.0009592fichero6MB18018ms (18 sec)
1.000.00078498memoria58MB1209350ms (20 min)
1.000.00078498fichero58MB1303905ms (21 min)


En los resultados se observa claramente que este método no es lineal y deja de ser útil para números grandes. El número de primos, por cierto, no incluye el '1'.

Antes de esta implementación, hice un intento en Perl por el sencillo algoritmo de dividir cada número entre todos los primos anteriores. Aunque la división es una operación más compleja y lenta que la multiplicación (como se utiliza en la criba de Eratóstenes), sin embargo el algoritmo da mejores resultados para números grandes. Aquí se puede ver una gráfica de los segundos que tarda en encontrar cada 10.000 números primos al calcular todos los primos por debajo de 100 millones.



La ejecución tardó 5.929 segundos (1 hora y 39 minutos) y dio como resultado que entre 0 y 100 millones hay 5.761.455 números primos.

prime.pl
#!/usr/bin/perl
#
# Prime number calculator
# @author José Cotrino (http://www.cotrino.com)
#
use strict;
use warnings;
use Benchmark;

open(OUT, ">primes.txt");
open(STAT, ">primes_stat.txt");
my $t0 = new Benchmark;

# first primes are 1, 2, 3, 5, 7...
# we will skip multiples of 1, 2 & 5 using other methods, not dividing
my @list = (3, 7);

# just for statistics purposes
my $n = 0;
my $m = 0;
my $previousTimer = new Benchmark;
my $currentTimer;

my $i = 11;
while( $i < 100000000 ) {
my $isPrime = 1;
my $i_text = $i."";
# avoid multiples of 5
if( $i_text =~ m/5$/ ) {
$isPrime = 0;
} else {
my $sqrt = sqrt($i); # divide $i against all primes lower or equal than sqrt($i)
for( my $j = 0; $j < scalar(@list) and $isPrime == 1 and $list[$j] <= $sqrt; $j++ ) {
if( $i % $list[$j] == 0 ) {
$isPrime = 0;
}
}
}

if( $isPrime == 1 ) {
push @list, $i;
print OUT "$i ";
# count the time it takes to calculate each 10000 prime numbers
$n++;
if( $n >= 10000 ) {
$n = 0;
$m++;
$currentTimer = new Benchmark;
if( timestr(timediff($currentTimer, $previousTimer)) =~ m/^(.+)\s+/ ) {
print STAT $m." ".$1."\n";
}
$previousTimer = $currentTimer;
}
}
$i += 2; # avoid multiples of 2
}

my $t1 = new Benchmark;
my $td = timediff($t1, $t0);
print "Execution took:",timestr($td),"\n";
print scalar(@list)." numbers found\n";
close(OUT);
close(STAT);

system("gnuplot plot.cfg");

domingo 1 de febrero de 2009

München - Campeon cubierto de nieve

Llegando al trabajo una mañana.

München - Campeon cubierto de nieve

Lástima no haber tenido una cámara de verdad a mano en lugar del móvil.