Leyendo feeds en un libro electrónico

Actualmente, leo la mayoría de los libros y blogs que sigo en un libro electrónico Cybook Gen3, que aún me parece el juguete más útil que he comprado en 2008, seguido muy de cerca por el Lego Mindstorms NXT.

En este tipo de dispositivo es recomendable usar formatos con reflow, es decir, documentos en los que el texto se adapte al ancho de pantalla y permitan cambiar el tamaño de la fuente. Uno de ellos es el formato MobiPocket (.prc, .mobi), parecido a HTML, pero que incluye imágenes y texto en un único archivo binario.

Sin embargo, la aplicación gratuita (aunque privativa) Mobipocket Reader, que se utiliza para convertir archivos PDF, HTML o fuentes de noticias RSS/Atom a este formato, sólo está disponible para Windows y es bastante inestable.

Harto de este programa, me he implementado mi propia aplicación en Java para descargar fuentes de noticias y enviarlas al libro electrónico. Para ello, hago uso de la librería Yarfraw, un parser de RSS/Atom, y de Mobiperl, una herramienta de conversión de HTML a MobiPocket, siguiendo este diagrama:

La aplicación bajo licencia GPL3 (código fuente y binario) está alojada en Google Code: feed2ereader, donde también se puede encontrar cómo instalarla y usarla.

En las siguientes fotos se puede ver el Cybook mostrando el índice navegable de un blog…

…Y el artículo de un blog incluyendo imágenes:

Muy recomendable para leer artículos largos como los habituales de El Cedazo o Historias de España en el metro, en la cama o en una nave espacial más allá del cinturón de Orión.

Descarga: feed2ereader

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

[c]
#include <omp.h>
#include <stdio.h>

// @author José Cotrino (https://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",N);
printf(" + cada iteración puede ser asignada teóricamente a un procesador\n");
printf(" + el array sobre el que se calcula está en memoria compartida\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]);
}
}
[/c]

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

[cpp]
#include <stdio.h>
#include <time.h>
#include <gmp.h>
#include <omp.h>

// @author José Cotrino (https://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);
}
}
[/cpp]

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úmero Primos Duración
100 25 0s
1.000 168 0s
10.000 1.229 0s
100.000 9.592 0s
1.000.000 78.498 0s
10.000.000 664.579 7s
100.000.000 5.761.455 66s (~1.1min)
1.000.000.000 50.847.534 703s (~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

[cpp]
#include <stdio.h>
#include <time.h>
#include <gmp.h>

// Using GMP library (http://gmplib.org/manual/)
// similar: http://stackoverflow.com/questions/622/most-efficient-code-for-the-first-10000-prime-numbers
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 %s %.0f\n",mpz_get_str(NULL,10,N_max),mpz_get_str(NULL,10,N_primes),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);
}
}

[/cpp]

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

Los resultados obtenidos son algo así:

Máximo número Primos Duración
100 25 0s
1.000 168 0s
10.000 1.229 0s
100.000 9.592 0s
1.000.000 78.498 1s
10.000.000 664.579 12s
100.000.000 5.761.455 126s (~2min)
1.000.000.000 50.847.534 1.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.

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

[java]
package com.cotrino.primes;

import java.sql.SQLException;

/**
* Prime number calculator using SQL
*
* @author José Cotrino (https://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 + " 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");
}
}
[/java]

DB.java

[java]
package com.cotrino.primes;

import java.sql.*;

/**
*
* SQLite database handler for prime number calculator
* @author José Cotrino
* (https://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 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!");
}
}
}
[/java]


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 numero Primos Fichero/Memoria Uso de memoria Duración
100 25 memoria 5MB 180ms
100 25 fichero 5MB 711ms
1.000 168 memoria 5MB 221ms
1.000 168 fichero 5MB 1042ms
10.000 1229 memoria 5MB 761ms
10.000 1229 fichero 5MB 1753ms
100.000 9592 memoria 6MB 15283ms (15 sec)
100.000 9592 fichero 6MB 18018ms (18 sec)
1.000.000 78498 memoria 58MB 1209350ms (20 min)
1.000.000 78498 fichero 58MB 1303905ms (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

[perl]
#!/usr/bin/perl
## Prime number calculator
# @author José Cotrino (https://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");

[/perl]

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.

Cargar más