Fattorizzazione di numeri interi con Java RMI

Università degli Studi di Salerno
Facoltà di Scienze Matematiche, Fisiche e Naturali
Corso di laurea Magistrale in Informatica
Corso di SISTEMI OPERATIVI II
Fattorizzazione di numeri interi con Java RMI
Studenti:
Valerio Cinque 0522500098
Francesco Testorio 0522500075
Domenico Viscito 0522500024
Docente:
Prof. G. Cattaneo
Anno Accademico 2010/2011
INDICE
1.
INTRODUZIONE .................................................................................................................................................. 2
1.1
2.
ALGORITMI DI FATTORIZZAZIONE ...................................................................................................................... 7
2.1
3.
ARCHITETTURA DI RMI ......................................................................................................................................... 11
RMI: IL PROCESSO DI CREAZIONE ............................................................................................................................ 12
IMPLEMENTARE UN COMPUTE SERVER .................................................................................................................... 13
IMPLEMENTAZIONE DEL CRIVELLO QUADRATICO IN JAVA RMI ................................................................................... 15
ESEMPIO DI ESECUZIONE DEL CRIVELLO QUADRATICO................................................................................................. 16
TEST FATTORIZZAZIONE CON IL CRIVELLO QUADRATICO IN JAVA RMI ..............................................................22
4.1
4.2
4.3
4.4
4.5
5
IL CRIVELLO QUADRATICO ....................................................................................................................................... 8
JAVA RMI ..........................................................................................................................................................10
3.1
3.2
3.3
3.4
3.5
4
CRITTOGRAFIA RSA ............................................................................................................................................... 3
CONFIGURAZIONE AMBIENTE ................................................................................................................................. 22
SCELTA DEGLI INTERI DA FATTORIZZARE .................................................................................................................... 27
“GRANULARITÀ DIMENSIONE SIEVE” ....................................................................................................................... 28
SCALABILITÀ........................................................................................................................................................ 30
NUMERO DI BIT DEGLI INTERI DA FATTORIZZARE ........................................................................................................ 32
CONCLUSIONI ....................................................................................................................................................35
1. Introduzione
Un numero primo è un intero, maggiore a uno, che è divisibile soltanto per uno e per se stesso,
per esempio 2, 3, 5, 7, 11, 13, . . . sono numeri primi. Ogni intero positivo si può scomporre, in
modo unico, a meno dell’ordine dei fattori, nel prodotto di numeri primi.
Per esempio:
30 = 2 * 3 * 5;
60 = 2 * 2 * 3 * 5;
6 = 3 * 2;
3300 = 22 * 31 *52 * 111 ;
10001 = 73 * 137.
Nei giorni nostri, con l’intensificarsi dei calcolatori elettronici e della loro potenza computazionale,
si cercano metodi efficienti per trovare la scomposizione in fattori di un dato intero. Questo
problema è d’interesse sia pratico sia teorico.
Questo problema si divide in due parti:
ο‚·
Decidere se un dato numero è primo o no: problema dei test di primalità. Questo test è
una procedura algoritmica che, dato un numero naturale 𝑛 in input, restituisce PRIME se 𝑛
è un numero primo, COMPOSITE se 𝑛 è un numero composto.
ο‚·
Determinare esplicitamente una scomposizione in fattori (non banali) di un numero
composto: problema della fattorizzazione. In particolare, fattorizzare o "ridurre in fattori"
un numero 𝑛, significa trovare un insieme di numeri {π‘Ž0 , π‘Ž1 , π‘Ž2 , … , π‘Žπ‘˜ } tali che il loro
prodotto sia il numero originario (𝑛 = π‘Ž0 ∗ π‘Ž1 ∗ π‘Ž2 ∗ … ∗ π‘Žπ‘˜ ).
Questi due problemi, anche se chiaramente legati l’uno all’altro, sono di natura differente. Allo
stato attuale, il problema del test di primalità è relativamente facile (algoritmo polinomiale nella
dimensione dell’input), mentre il secondo sembra essere piuttosto difficile.
La difficoltà nel fattorizzare numeri molto grandi è alla base di alcuni sistemi crittografici moderni,
per esempio RSA, che sono utilizzati per garantire la privacy nella trasmissione di documenti
riservati, la segretezza delle e-mail (per esempio, PGP), la sicurezza nel commercio elettronico,
ecc.
Trovare i fattori di un numero intero grande è un’impresa assai ardua, e può essere impossibile
date le risorse disponibili. Come detto, non si conoscono metodi polinomiali per la fattorizzazione,
come invece accade per i test di primalità.
1.1 Crittografia RSA
La crittografia si occupa delle "scritture nascoste", in altre parole dei metodi per rendere un
messaggio "offuscato" in modo da non essere comprensibile a persone non autorizzate a leggerlo.
La crittografia tratta della lettura di messaggi dati in forma cifrata, cosicché solo i destinatari siano
in grado di decifrarli e di leggerli. Si tratta di una materia molto antica, che si sviluppa
specialmente in tempo di guerra.
In crittografia l'acronimo RSA indica un algoritmo di crittografia asimmetrica, utilizzabile per cifrare
o firmare informazioni. Un sistema di crittografia asimmetrico si basa sull'esistenza di due chiavi
distinte, che sono usate per cifrare e decifrare. Se una chiave è usata per la cifratura, l’altra deve
necessariamente essere utilizzata per la decifratura. La questione fondamentale è che, nonostante
le due chiavi siano fra loro dipendenti, non sia possibile risalire dall'una all'altra, in modo che se
anche si è a conoscenza di una delle due chiavi, non si possa risalire all'altra, garantendo in questo
modo l'integrità della crittografia.
Per realizzare con il cifrario asimmetrico un sistema crittografico pubblico, è importante che un
utente si crei autonomamente entrambe le chiavi, denominate "diretta" e "inversa", e ne renda
pubblica una soltanto. Così facendo si viene a creare una sorta di "elenco telefonico" a
disposizione di tutti gli utenti, che raggruppa tutte le chiavi dirette, mentre quelle inverse saranno
tenute segrete dagli utenti che le hanno create e da questi utilizzate solo quando ricevono un
messaggio cifrato con la rispettiva chiave pubblica dell’ "elenco" da parte di un certo mittente,
ottenendo in questo modo i presupposti necessari alla sicurezza del sistema.
I ricercatori del MIT, Ronald Rivest, Adi Shamir e Leonard Adleman, nel 1978 hanno saputo
implementare tale logica utilizzando particolari proprietà formali dei numeri primi con alcune
centinaia di cifre. L'algoritmo da loro inventato, denominato RSA, acronimo indicante le iniziali dei
loro cognomi, non è sicuro da un punto di vista matematico teorico, perché esiste la possibilità che
tramite la conoscenza della chiave pubblica si possa decifrare un messaggio. L’enorme dispendio
in termini di tempo necessario per risolvere dei calcoli, fa di quest’algoritmo un sistema
abbastanza affidabile, perché c’è bisogno di eseguire una fattorizzazione di un numero intero
molto grande al fine di decifrare il messaggio.
Come detto, RSA è basato sull'elevata complessità computazionale della fattorizzazione in numeri
primi. Il suo funzionamento base è il seguente:
ο‚·
Si scelgono a caso due numeri primi, 𝒑 e 𝒒, l'uno indipendentemente dall'altro, abbastanza
grandi da garantire la sicurezza dell'algoritmo.
ο‚·
Si calcola il loro prodotto 𝒏 = 𝒑 ∗ 𝒒, chiamato modulo, perché tutta l'aritmetica seguente
è modulo 𝑛.
ο‚·
Si sceglie poi un numero 𝒆 chiamato esponente pubblico, coprimo e più piccolo di
(𝒑 − 𝟏) ∗ (𝒒 − 𝟏).
In matematica, gli interi π‘Ž e 𝑏 si dicono coprimi o primi tra loro se e solo se essi non hanno
nessun divisore comune eccetto 1 e −1, o, equivalentemente, se il loro massimo comun
divisore è 1. Per esempio, 6 e 35 sono coprimi, ma 6 e 27 non lo sono perché entrambi
sono divisibili per 3; 1 è coprimo con ogni numero intero; 0 è coprimo solo ad 1 e −1.
ο‚·
Infine, si calcola il numero 𝒅, chiamato esponente privato:
𝒆 ∗ 𝒅 ≡ 𝟏 (π’Žπ’π’…((𝒑 − 𝟏) ∗ (𝒒 − 𝟏)))
La chiave pubblica è la coppia (𝒏, 𝒆), mentre la chiave privata è (𝒏, 𝒅). I fattori 𝑝 e π‘ž possono
essere distrutti, anche se spesso sono memorizzati all’interno della chiave privata.
La forza dell’algoritmo sta nel fatto che per calcolare 𝒅 da 𝒆 o viceversa, non basta la conoscenza
di 𝒏, ma serve il valore (𝒑 − 𝟏) ∗ (𝒒 − 𝟏), infatti fattorizzare, cioè scomporre l’intero nei suoi
divisori, è un’operazione molto lenta, ma soprattutto l’operazione di modulo non è invertibile,
dunque anche conoscendo 𝒏 non si può risalire al prodotto modulo 𝒏 di 𝒑 e 𝒒.
Un messaggio originario π’Ž è cifrato attraverso l’operazione π’Žπ’† (π’Žπ’π’… 𝒏), la quale restituisce il
messaggio cifrato 𝒄. Quest’ultimo è decifrato con l’operazione 𝒄𝒅 = π’Žπ’†∗𝒅 = π’ŽπŸ (π’Žπ’π’… 𝒏).
Il procedimento funziona solo se la chiave 𝒆 utilizzata per cifrare e la chiave 𝒅 utilizzata per
decifrare sono legate tra loro dalla relazione in precedenza indicata, cioè 𝒆 ∗ 𝒅 ≡ 𝟏 (π’Žπ’π’…((𝒑 −
𝟏) ∗ (𝒒 − 𝟏))), e quindi quando un messaggio viene cifrato con una delle due chiavi può essere
decifrato solo utilizzando l'altra.
Tuttavia proprio qui si vede la debolezza dell'algoritmo: si basa sull'assunzione mai dimostrata
𝒆
(RSA assumption) che il problema di calcolare √𝒄 π’Žπ’π’… 𝒏 con 𝒏 numero composto di cui non si
conoscono i fattori, sia non trattabile da punto di vista computazionale.
Invece, per quanto riguarda la firma digitale, il messaggio è crittografato con la chiave privata, in
modo che chiunque possa, utilizzando la chiave pubblica (conosciuta da tutti), decifrarlo e, oltre a
poterlo leggere in chiaro, essere certo che il messaggio è stato inviato dal possessore della chiave
privata corrispondente a quella pubblica utilizzata per leggerlo.
Per motivi di efficienza e comodità, il messaggio normalmente è inviato in chiaro con allegata la
firma digitale di un hash del messaggio stesso. In questo modo il ricevente può direttamente
leggere il messaggio (che è in chiaro) e può comunque utilizzare la chiave pubblica per verificare
che l'hash ricevuto sia uguale a quello calcolato localmente sul messaggio ricevuto. Se i due hash
coincidono, allora anche il messaggio completo corrisponde, questo è vero solo se l'hash utilizzato
è sicuro dal punto di vista della crittografia.
La seguente tabella mostra alcuni esempi di RSA Factoring Challenge, ossia le sfide volte a
fattorizzare dei numeri RSA.
Numero
RSA
RSA-100
Cifre
decimali
100
Cifre
binarie
330
RSA-110
110
364
RSA-120
RSA-129
RSA-130
RSA-140
RSA-150
RSA-155
120
129
130
140
150
155
397
426
430
463
496
512
RSA-160
160
530
RSA-170
170
563
RSA-576
174
576
RSA-180
180
596
RSA-190
190
629
RSA-640
193
640
RSA-200
200
663
RSA-210
210
696
RSA-704
RSA-220
RSA-230
RSA-232
212
220
230
232
704
729
762
768
Premio
offerto
$100 USD
$10,000 USD
$20,000 USD
$30,000 USD
Data
fattorizzazione
Aprile 1991
Fattorizzato da
Arjen K. Lenstra
Arjen
K.
Lenstra e M.S.
Aprile 1992
Manasse
Giugno 1993
T. Denny et al.
Aprile 1994
Arjen K. Lenstra et al.
10 aprile 1996
Arjen K. Lenstra et al.
2 febbraio 1999 Herman J. J. te Riele et al.
16 aprile 2004
Kazumaro Aoki et al.
22 agosto 1999
Herman J. J. te Riele et al.
Jens Franke et al., Università
1º aprile 2003
di Bonn
29
D. Bonenberger and M.
dicembre 2009
Krone
Jens Franke et al., Università
3 dicembre 2003
di Bonn
S. A. Danilov and I. A.
8 maggio 2010
Popovyan, Università statale
di Mosca
non ancora fattorizzato
Jens Franke et al., Università
Novembre 2005
di Bonn
Jens Franke et al., Università
9 maggio 2005
di Bonn
non ancora fattorizzato
non ancora fattorizzato, premio ritirato
non ancora fattorizzato
non ancora fattorizzato
non ancora fattorizzato
12
RSA-768 232
768
$50,000 USD
Thorsten Kleinjung et al.
dicembre 2009
Nella tabella RSA Factoring Challenge (http://it.wikipedia.org/wiki/RSA_Factoring_Challenge).
2. Algoritmi di fattorizzazione
Nel corso della storia sono stati ideati molti algoritmi per rendere la fattorizzazione un problema
risolvibile sempre più veloce dal punto di vista computazionale, però, tuttora rimane un problema
complesso.
Tra i metodi più popolari ricordiamo:
ο‚·
Metodo forza bruta: si divide l’intero da fattorizzare 𝑛 per tutti i numeri che gli sono
minori. Il costo operativo nel caso peggiore è 𝑂(𝑛).
ο‚·
Metodo forza bruta migliorato: si considerano solo i numeri primi minori o uguali alla
radice quadrata del numero 𝑛. Si prova a dividere il numero 𝑛 per il minore di questi, se
non risulta divisibile si procede con il successivo e così via. Si procede allo stesso modo con
il risultato ottenuto e si ripetono le stesse operazioni fino a quando si ottiene quoziente 1.
Se tutti i numeri primi minori della radice quadrata di 𝑛 sono stati provati e nessuno di loro
è un divisore, 𝑛 stesso è un numero primo. Il costo operativo nel caso peggiore è 𝑂(√𝑛).
ο‚·
Metodo delle curve ellittiche (ECM): uno dei più noti è quello di Lenstra, che si basa su idee
già contenute nel "(p-1)-method" di Pollard. Insieme all'algoritmo di Pollard-Strassen e al
Crivello dei campi di numeri generale è a tutt'oggi uno dei più veloci metodi totalmente
deterministici.
ο‚·
Metodi probabilistici: tra di essi ci sono gli algoritmi di Schnorr-Lenstra e di LenstraPomerance.
ο‚·
Crivello Quadratico: é un moderno algoritmo di fattorizzazione d’interi ed è il secondo
metodo più veloce conosciuto, dopo il General Number Field Sieve. E’ il più veloce per interi
sotto le 100 cifre decimali circa, ed è più semplice del number field sieve. Dato un intero 𝑛
da fattorizzare, l’algoritmo restituisce due fattori, non necessariamente primi. Questo
metodo si adatta molto bene alla fattorizzazione degli interi di RSA, poiché questi numeri
sono il prodotto di due primi. Infatti, molte RSA Factoring Challenge hanno adoperato
proprio questo metodo ottenendo buoni risultati.
2.1 Il Crivello Quadratico
Il Crivello Quadratico (CQ) è un algoritmo di fattorizzazione creato da Carl Pomerance, un
matematico statunitense, studioso di teoria dei numeri. Quest’algoritmo è particolarmente
famoso perché nel 1994 ha fattorizzato il numero RSA-129, composto da 129 cifre in base dieci.
RSA-129 = 11438162575788886766923577997614661201021829672124236256256184293
5706935245733897830597123563958705058989075147599290026879543541
RSA-129 = 3490529510847650949147849619903898133417764638493387843990820577
× 32769132993266709549961988190834461413177642967992942539798288533
La sfida per la fattorizzazione includeva un messaggio da decifrare con RSA-129. Una volta
decriptato, usando il crivello quadrico, il messaggio recuperato fu:
"The Magic Words are Squeamish Ossifrage"
(Le parole magiche sono un avvoltoio schizzinoso).
L'algoritmo tradizionale consta principalmente di otto passi:
1) Viene dato in input il numero naturale intero dispari 𝑛 > 1. Se l’intero è pari, allora uno dei
fattori è sempre 2.
2) Si sceglie un naturale π‘˜ > 0.
𝑛
3) Si esaminano tutti i numeri primi 𝑝 ≤ π‘˜ utilizzando il criterio di Eulero (𝑝) e sfruttando il
𝑛
simbolo di Legendre si eliminano i primi dispari tali che (𝑝) ≠ 1. Da questo procedimento
si ottiene così la base di fattori 𝐡 = 𝑝1 , 𝑝2 , … , 𝑝𝑑 con 𝑝𝑖 numero primo.
4) Facendo assumere a π‘Ÿ valori interi successivi a ⌊𝑛⌋, si trovano almeno 𝑑 + 1 valori
𝑦 = π‘Ÿ 2 − 𝑛 che abbiano tutti i loro fattori primi in 𝐡. Ogni valore 𝑦 è detto sieve.
5) Per ognuno dei valori 𝑦1 , 𝑦2, … , 𝑦𝑑+1 si calcola il vettore 𝕫𝑑2 ∢ 𝑣2 (𝑦𝑖 ) = (𝑒1 , 𝑒2 , … , 𝑒𝑑 ) dove
𝑒𝑖 è la riduzione modulo 2 dell’esponente di 𝑝𝑖 nella fattorizzazione di 𝑦𝑖 .
6) Con il metodo di eliminazione di Gauss si determinano alcuni dei vettori 𝑣2 (𝑦𝑖 ) che
producono la somma uguale al vettore nullo.
In matematica, il metodo di eliminazione di Gauss è un algoritmo usato in algebra lineare
per determinare le soluzioni di un sistema di equazioni lineari, per calcolare il rango o
l'inversa di una matrice. L'algoritmo, attraverso l'applicazione di operazioni elementari
dette mosse di Gauss, riduce la matrice in una forma detta “a scalini”. La matrice così
ridotta permette il calcolo del rango della matrice (che sarà pari al numero di scalini/pivot)
e la risoluzione del sistema lineare a essa associato.
7) Si pone π‘₯ uguale al prodotto degli π‘Ÿπ‘– corrispondenti agli 𝑦𝑖 trovati nel passo 6) e si pone 𝑦
uguale al prodotto delle potenze di 𝑝1 , 𝑝2 , … , 𝑝𝑑 con esponenti uguali alla semisomma
degli esponenti della fattorizzazione degli stessi 𝑦𝑖 .
8) Si calcola 𝑑 = 𝑔𝑐𝑑(π‘₯ − 𝑦, 𝑛) e se 1 < 𝑑 < 𝑛 allora 𝑑 è divisore non banale di 𝑛, altrimenti
si torna al passo 2) con una scelta di π‘˜ più grande. Il massimo comune divisore (M.C.D. o
𝑔𝑐𝑑) di due numeri interi, che non siano entrambi uguali a zero, è il numero naturale più
grande per il quale possono entrambi essere divisi.
Il tempo di esecuzione (running time) del Crivello Quadratico dipende solo dalla dimensione di 𝑛, il
numero da fattorizzare, e non da speciali strutture o proprietà dell’algoritmo.
𝑂(𝑛) = 𝑒 √ln(𝑛)∗ln(ln(𝑛))
Come ci aspettavamo il running time è esponenziale nella dimensione dell’input.
3. Java RMI
Abbiamo implementato l’algoritmo del Crivello Quadratico (CQ) utilizzando RMI, cioè il modello ad
oggetti distribuito offerto da Java.
Il modello Java RMI si integra all’interno della semantica del linguaggio ed in pratica, non è altro
che un'estensione "ad oggetti" di Remote Procedure Call e fornisce la visione astratta di una
chiamata di procedura remota verso altri processi.
Gli scopi di RMI sono:
οƒΌ Supportare in maniera trasparente invocazioni remote di metodi su oggetti su differenti
JVM;
οƒΌ Integrare in maniera naturale il modello distribuito all'interno di Java mantenendo le
caratteristiche del linguaggio;
οƒΌ Rendere evidenti le differenze tra il modello distribuito e quello locale;
οƒΌ Rendere facile (compatibilmente con gli altri scopi) la realizzazione di applicativi distribuiti
in Java;
οƒΌ Mantenere la sicurezza offerta da Java con i security manager e i class loader.
Generalmente le applicazioni sviluppate con questo modello sono composte da server e client. Il
server crea un certo numero di oggetti, li rende accessibili da remoto ed attende le invocazioni dei
client sugli oggetti; mentre il client: preleva il riferimento ad uno o più oggetti remoti ed invoca i
loro metodi.
RMI fornisce il meccanismo attraverso il quale server e client comunicano, quindi un'applicazione
distribuita deve poter:
οƒΌ Localizzare oggetti remoti (attraverso un registro);
οƒΌ Comunicare con oggetti remoti (nascondendo, per quanto possibile, i dettagli
dell'invocazione remota);
οƒΌ Caricare dinamicamente classi dalla rete.
Un semplice schema di funzionamento per RMI è quello mostrato in figura:
Figura 3.1 - Schema di funzionamento per RMI
In quest'ambito il server registra l'oggetto con un nome sul registry(1), il client ricerca ed ottiene il
riferimento remoto dell'oggetto(2) ed invoca il suo metodo remoto(3). I passi 4 e 5 mostrano il
caricamento dinamico delle classi.
3.1 Architettura di RMI
La figura sottostante mostra la struttura di un’applicazione RMI che è organizzata in strati
orizzontali sovrapposti
Figura 3.2 - Architettura di RMI
Lo strato più alto è costituito da applicazioni (client e server) eseguite da JVM differenti. Lo stub e
lo skeleton forniscono la rappresentazione dell’oggetto remoto: lo stub gestisce la simulazione
locale sul client e agendo come proxy consente la comunicazione con l’oggetto remoto; lo
skeleton ne consente l’esecuzione sul server. Il client esegue i metodi dell’oggetto remoto in modo
del tutto analogo alla chiamata locale, senza preoccuparsi dei dettagli della comunicazione. Il
Remote Reference Layer (RRL) ha il compito di instaurare un connessione virtuale fra il client e il
server (esegue operazioni di codifica e decodifica dei dati). Questo adotta un protocollo generico e
indipendente dal tipo di stub o skeleton utilizzato. Il Transport Layer esegue la connessione vera e
propria tra le macchine utilizzando le funzionalità standard di networking di Java, ovvero i socket
(protocollo TCP/IP).
3.2 RMI: il processo di creazione
Il processo di creazione può essere suddiviso nei seguenti passi:
1. Il server dichiara i servizi offerti attraverso un interfaccia java remota che estende
java.rmi.Remote. Ogni metodo ti tale interfaccia deve lanciare l’eccezione
java.rmi.RemoteException.
2. Il server implementa l’interfaccia remota del punto 1; in più gli oggetti devono derivare de
java.rmi.UnicastRemoteObject.
3. La classe server viene compilata generando il .class.
4. Usando rmic (stub compiler) sulla classe server, viene generato un client stub e un server
stub (skeleton): lo stub invia le chiamate remote verso il server effettuando il marshalling
dei parametri; lo skeleton, speculare allo stub, riceve le chiamate remote, effettua
l’unmarshalling e chiama effettivamente i metodi dell’oggetto.
5. Per rendere disponibili gli oggetti distribuiti e il loro recupero, java mette a disposizione un
servizio di Naming attraverso l’applicazione rmiregistry.
6. Il server viene mandato in esecuzione e crea gli oggetti che devo essere acceduti da
remoto.
7. Ogni oggetto da accedere da remoto deve essere registrato sul registry lanciato al punto 5.
Per far ciò si usano i metodi di java.rmi.Naming che consentono di effettuare il binding di
nomi ad oggetti.
8. Una volta implementato il client, questi usa i metodi di java.rmi.Naming per localizzare un
oggetto remoto. L’invocazione dei metodi usa lo stub come intermediario.
9. Il client viene compilato ed eseguito e lo stub garantisce l’accesso agli oggetti remoti.
Figura 3.3 – Processo di creazione per RMI
3.3 Implementare un Compute Server
In questa paragrafo mostreremo come implementare un compute server perché questa
implementazione è stata utilizzata per affrontare il problema.
Un compute server è un oggetto remoto che consente ad un server di ricevere dei task dai client,
eseguirli e restituire il risultato. Il task viene definito dal client ma viene eseguito sulla macchina
del server. Esso può variare indipendentemente dal server, l’importante è che rispetti una
determinata interfaccia.
Possiamo riassumere i compiti del compute server in 3 semplici passi:
1. Il compute server scarica dal client il codice del task
2. Lo esegue all’interno della propria Java virtual machine
3. Restituisce al client il risultato
Per implementare un compute server servono due interfacce:
 L’interfaccia Compute, che consenta ai client di inviare task al compute server
 L’interfaccia Task, che consenta al compute server di eseguire le varie operazioni.
Per capire meglio le interfacce di seguito sono stati riportati le loro implementazioni. Iniziamo
dall’interfaccia Compute:
package compute;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Compute extends Remote {
Object executeTask ( Task t ) throws RemoteException;
}
Questa interfaccia definisce i metodi che possono essere chiamati da altre virtual machine. Gli
oggetti che implementano questa interfaccia diventano oggetti remoti.
Interfaccia Task:
package compute;
import java.io.Serializable;
public interface Task extends Serializable {
Object execute ();
}
Questa interfaccia è usata come argomento nel metodo executeTask dell’interfaccia Compute e
fornisce al Compute Server il meccanismo per eseguire il task. Non è un interfaccia remota, quindi
non è associata ad oggetti remoti. Ovviamente l’interfaccia estende Serializable in quanto
necessita della trasformazione automatica di oggetti e strutture in sequenze di byte manipolabili.
Definite queste interfacce l’implementazione del server risulta essere molto semplice infatti
implementa l’interfaccia compute e comprende il metodo main per l’esecuzione.
public class Server extends UnicastRemoteObject implements Compute {
public Server () throws RemoteException {
super ();
}
public Object executeTask ( Task t ) {
return t.execute ();
}
}
L’implementazione del task da far eseguire (Task.java) è la classe che contiene il metodo da far
eseguire al server. Ora il client che ha a disposizione questa classe può farla eseguire al server. È
necessario istruire il server attraverso una policy (-Djava.security.policy = policy).
Figura 3.4 – Argomenti della VM del Server
3.4 Implementazione del Crivello Quadratico in JAVA RMI
Il progetto è costituito da due classi java principali: Client e CQReducer.
ο‚·
ο‚·
Client: è la classe principale del progetto e contiene il metodo main e costituisce la fase di
inizializzazione e di divisione del lavoro. In particolare, si inserisce il numero da fattorizzare
e viene generato il file d’input contenente i Sieve e tramite il metodo avviaThread() viene
diviso il lavoro tra tutti i servizi di fattorizzazione disponibili nella rete;
CQReducer: dopo aver collezionato in una lista tutti gli elementi smooth calcolati dai
thread, la classe CQReducer realizza con tali valori un sistema lineare modulo 2. Il suo
obiettivo è risolvere il sistema adoperando tecniche come l’eliminazione di Gauss e la
sostituzione all’indietro. Il risultato ci permette di trovare i fattori dell’intero dato in input.
Per l’implementazione abbiamo utilizzato i thread di Java per rendere la chiamata al metodo run()
del thread asincrona: quando mandiamo in esecuzione la computazione su un Server, il thread a
cui è associato aspetta il risultato, lasciando così libero il client di avviare la computazione su altri
Server.
3.5 Esempio di esecuzione del Crivello Quadratico
Il numero che vogliamo fattorizzare utilizzando l’algoritmo del crivello quadratico è il seguente
𝑡 = πŸ–πŸ•πŸ’πŸ”πŸ‘.
Come prima operazione bisogna identificare il limite dei fattori di base (numeri primi p) da
utilizzare per la fattorizzazione del numero, questo avviene utilizzando il metodo
getLimiteFattoriBase. Il metodo indicato calcola il numero ottimale dei fattori di base per
fattorizzare il numero dato in input utilizzando la seguente formula: (𝑒 (√ln 𝑁∗ln ln 𝑁) )√2/4. Nel
seguente esempio il limite dei fattori di base da utilizzare per la fattorizzazione del numero πŸ–πŸ•πŸ’πŸ”πŸ‘
è πŸ•, cioè questo significa che dobbiamo utilizzare i primi sette fattori di base che attraverso il
criterio Eulero:
𝑝−1
𝑁
(
)
( ) ≡ 𝑁 2 (mod p)
𝑝
𝑁
Soddisfano il simbolo di Legendre, ossia ( ) ≡ 1 π‘šπ‘œπ‘‘ 𝑝.
𝑝
Partendo dal numero primo 𝑝 = 2 applichiamo il criterio di Eulero e consideriamo solo i primi
sette valori che soddisfano il simbolo di Legendre (dove 𝑁 è l’intero e 𝒑 un numero primo), cioè
che restituiscono in modulo il valore 1. La seguente operazione è eseguita dal metodo statico
legendre:
𝑝
2
3
5
7
11
13
17
19
23
29
31
37
41
𝑁
𝑝
1
1
-1
-1
-1
1
1
1
-1
1
-1
-1
1
I nostri fattori di base sono i seguenti 𝐡 = {2,3,13,17,19,29,41}, dove |𝐡| = 7.
La fase successiva consiste nell’eseguire la fase di sieving, cioè del calcolo dei sieve. Prima di
calcolare i sieve però bisogna identificare il numero di sieve necessari per la fattorizzazione del
nostro numero, questo avviene utilizzando il metodo getSize_fullSieveInterval.Il metodo calcola il
numero di sieve utilizzando la seguente formula: π‘›π‘’π‘šπ‘’π‘Ÿπ‘œπΉπ‘Žπ‘‘π‘‘π‘œπ‘Ÿπ‘–π΅π‘Žπ‘ π‘’ 3 . Il numero di sieve
necessari per fattorizzare πŸ–πŸ•πŸ’πŸ”πŸ‘ è 343.
Ogni sieve è realizzata utilizzando la seguente formula:
𝑍(𝑋) = (𝑋 + ⌊√𝑁⌋)
π‘Œ(𝑋) = (𝑍(𝑋))2 − 𝑁
Per minimizzare i valori consideriamo la seguente formula:
𝑍(𝑋) = (𝑋 + [⌊√𝑁⌋ − (π‘›π‘’π‘šπ‘†π‘–π‘’π‘£π‘’⁄2)])
π‘Œ(𝑋) = (𝑍(𝑋))2 − 𝑁 = (𝑋 + 124)2 − 87463
Realizziamo il nostro vettore di sieve formato dalle coppie [𝑍(𝑋), π‘Œ(𝑋)] formato dalle sieve
computate per 0 ≤ 𝑋 < 343
V = { [Z(0),Y(0)] ; [Z(1), Y(1)] ; [Z(2),Y(2)] ……… [Z(342),Y(342)]} = {[124, -72087] ; [125, 71838]; [126, -71587]; ……… [466, 129693] }
L’operazione presentata è eseguita dal metodo writeSieveFileInput2 che genera il file delle Sieve in
cui per ogni riga viene specificato un intervallo la cui dimensione viene settata variando il
parametro “Dimensione Sieve”.
Una volta realizzato l’array di Sieve di ogni intervallo con il metodo createSieve, per ogni fattore di
base eseguiamo l’analisi su di esso in modo tale da individuare valori B-smooth. Questo avviene
utilizzando l’algoritmo di Shanks-Tonelli implementato dal metodo execute dell’oggetto Factorize
incorporato in un thread.
Ogni macchina Server lavorerà su un sottoinsieme dell’array di sieve generato.
Partendo dal primo fattore di base determinato nella fase iniziale, nel nostro esempio 2,
utilizzando il metodo getFirstIndexFactor per individuare il primo sieve che è un divisore di 2,
utilizziamo la seguente formula:
(𝑋 + 124)2 − 87463 ≡ 0 π‘šπ‘œπ‘‘ 2
Partendo dal valore seguente, in questo caso -71838 il quale si trova nella seconda posizione
dell’array, individuiamo il massimo esponente di 2 per cui esso è divisibile:
−71838 mod 2 = 0
−71838 π‘šπ‘œπ‘‘ 4 = 2
Quindi, il massimo esponente di 2 per -71838 è 1. Memorizziamo l’esponente e sostituiamo il
vecchio valore con il nuovo (cioè quello restituito dalla divisione del sieve che stiamo analizzando
con il massimo esponente di 2 per cui esso è divisibile).
V = { [Z(0),Y(0)] ; [Z(1), Y(1)] ; [Z(2),Y(2)]; ……… [Z(342),Y(342)]} = {[124, -72087]; [125,
-35919]; [126, -71587]; ……… [466, 129693] }
Il prossimo valore che andremo ad analizzare nella lista di sieve è dato dall’indice iniziale con un
incremento di due (l’incremento è dato dal valore del fattore di base) fino ad arrivare alla fine
dell’array, cioè il prossimo valore che analizzeremo è quello nella quarta posizione dell’array.
La precedente operazione è eseguita per tutti i fattori di base determinati nella fase iniziale. Il
risultato finale è il seguente:
[124, -24029]=>(3^1) x-24029 = -72087
[125, -307] => (2^1 x 3 ^ 2 x 13^1) x -307
[126, -4211] => (17^1) x-4211
[127, -1321] => (2^1 x 3 ^ 3)x -1321
[128, -43] => (3^1 x 19^1 x 29^1)x-43
[129, -2083] => (2^1 x 17^1)x-2083
[130, -23521] => (3^1)x-23521
[131, -11717] => (2^1 x 3^1)x-11717
[132, -70039] => -70039 (primo)
[133, -401] => (2^1 x 3^1 x 29^1)x-401
[134, -7723] => (3 ^ 2)x-7723
[135, -2663] => (2^1 x 13^1)x-2663
[136, -7663] => (3^2)x-7663
[137, -11449] => (2^1 x 3^1)x-11449
[138, -277] => (13^1 x 19^1)x-277
[139, -277] => (2^1 x 3^1 x 41^1)x-277
[140, -22621] => (3^1)x-22621
[141, -33791] => (2^1)x-33791
[142, -22433] => (3^1)x-22433
[143, -73] => (2^1 x 3 ^ 3 x 17^1)x-73
[144, -66727] => -66727 (divisibile per nessun fattore di base)
[145, -3691] => (2^1 x 3 ^ 2)x-3691
[146, -1297] => (3^1 x 17^1)x-1297
[147, -1733] => (2^1 x 19^1)x-1733
[148,-1] => (3^1 x 13^1 x 41 ^ 2)x-1
……….
……….
……….
Gli output generati da ogni macchina Server, sono le coppie originarie analizzate tale che il
secondo valore della coppia al termine della procedura descritta è 1 o -1.
Una volta terminata la seguente operazione avviene la fase di risoluzione del sistema. Il suo
obiettivo consiste nel risolvere il seguente sistema, cioè nel trovare il numero di colonne
linearmente indipendenti:
0
[1
1
0 0 1
1 1 0] βˆ™ 𝑆 ≡ [0 0 0 0] (π‘šπ‘œπ‘‘ 2)
1 1 1
La prima operazione è quella di realizzare la matrice attraverso le sieve che sono divisibili per i
fattori di base, cioè sono le sieve che nell’operazione precedente sono state sostituite con il valore
-1 oppure con 1. Si mostra sottolista dei sieve che soddisfano il criterio evidenziato:
[148,-1] => -1* (3^1 x 13^1 x 41 ^ 2)
[157,-1] => -1*(2^1 x 3^1 x 19 ^ 2 x 29^1)
[242,-1] => -1* (3 ^ 2 x 13 ^ 2 x 19^1)
[262,-1] => -1* (3 ^ 3 x 17^1 x 41^1)
[265,-1] => -1* (2^1 x 3^1 x 13 ^ 2 x 17^1)
[271,-1] => -1* (2^1 x 3 ^ 2 x 19^1 x 41^1)
[278,-1] => -1* (3 ^ 3 x 13^1 x 29^1)
[296, 1] => (3 ^ 2 x 17^1)
[299, 1] => (2^1 x 3^1 x 17^1 x 19^1)
[307, 1] => (2^1 x 3 ^ 2 x 13^1 x 29^1)
[316, 1] => (3 ^ 6 x 17^1)
[347, 1] => (2^1 x 3^1 x 17 ^ 2 x 19^1)
[385, 1] => (2^1 x 3^1 x 13^1 x 19^1 x 41^1)
[394, 1] => (3^1 x 19^1 x 29^1 x 41^1)
[413, 1] => (2^1 x 3 ^ 7 x 19^1)
Con i seguenti valori realizziamo la matrice dove le righe consisteranno nei fattori di base
{2,3,13,17,19,29,41} ed le colonne gli interi che sono associati alle sieve con valore -1 e 1
{148,157,242,262,265,271,278,296,299,307,316,347,385,394,413}. Infine, i valori saranno gli
esponenti dei fattori di base per ogni numero in modulo 2:
2
3
13
17
19
29
41
148
0
1
1
0
0
0
0
157
1
1
0
0
0
1
0
242
0
0
0
0
1
0
0
262
0
1
0
1
0
0
1
265
1
1
0
1
0
0
0
271
1
0
0
0
1
0
1
278
0
1
1
0
0
1
0
296
0
0
0
1
0
0
0
299
1
1
0
1
1
0
0
307
1
0
1
0
0
1
0
316
0
0
0
1
0
0
0
347
1
1
0
0
1
0
0
385
1
1
1
0
1
0
1
394
0
1
0
0
1
1
1
413
1
1
0
0
1
0
0
L’operazione indicata viene effettuata dal metodo realizzazioneMatriceSistema presente nella
classe CQReducer. Per esempio la realizzazione della prima colonna è data scomponendo il valore
assoluto del sieve (1482 − 87463) nei sui fattori: 2^0 x 3^1 x 13^1 x 17^0 x 19^0 x 29^0 x 41^2.
Quindi, inseriamo per ogni fattore il suo esponente nella casella opportuna in modulo 2. Inseriamo
1 nella casella associata a 3 e 13, mentre nelle altre 0.
In seguito, uniamo alla matrice il vettore dei termini noti:
b’ = [ 0 0 0 0 0 0 0]
( ‘ = trasposto )
Risolviamo il sistema con il metodo solve della classe BinaryLinearSystem. Il sistema può restituire
più di una soluzione, dove alcune potrebbero esser non esatte per il nostro scopo.
All’interno
del
metodo
solve
è
presente
l’invocazione
del
metodo
trasforma_in_TrangolareSuperiore, il quale trasforma la matrice in triangolare superiore
utilizzando il metodo dell’eliminazione di Gauss. Dopo aver eseguito il metodo indicato,
applichiamo la tecnica della sostituzione all’indietro per ottenere la soluzione del sistema. La
soluzione consiste nel vettore S nel quale i valori posti a 1 indicano le colonne linearmente
indipendenti:
S’ = [ 0 0 0 0 1 0 1 1 0 1 0 0 0 0 0]
In questo caso le colonne linearmente indipendenti hanno indice: 5, 7, 8, 10.
considerare i valori nelle intestazioni di queste colonne: 265, 278, 296, 307.
Dobbiamo
Una volta ottenuto il vettore S applichiamo la tecnica del massimo comune divisore MCD (GCD)
gcd(X-Y, N) ed gcd(X+Y, N) e verifichiamo se uno dei due restituisce un divisore. Dove:
1) 𝑁 indica il numero da fattorizzare (𝑁 = 87463).
2) 𝑋 è calcolato utilizzando la seguente formula: moltiplichiamo le colonne che sono
linearmente indipendenti: 𝑋 = 265 π‘₯ 278 π‘₯ 296 π‘₯ 307 = 6694540240.
3) π‘Œ
è
calcolato
utilizzando
la
seguente
formula:
π‘Œ = √(2652 − 87463) ∗ (2782 − 87463) ∗ (2962 − 87463) ∗ (3072 − 87463) =
13497354.
gcd(𝑋 − π‘Œ, 𝑁) = 𝑔𝑐𝑑((6694540240 − 13497354), 87463) = πŸπŸ’πŸ—
Per trovare l’altro fattore applichiamo la divisione: 87463/149 = πŸ“πŸ–πŸ•.
Quindi i fattori per 87463 sono 149 e 587. Infatti, 149 ∗ 587 = 87463.
Figura 3.5 In figura viene mostrato la console del client con i risultati della fattorizzazione del
numero 87463
4 Test Fattorizzazione con il Crivello Quadratico in JAVA RMI
4.1 Configurazione ambiente
L’algoritmo è stato testato nel laboratorio didattico P13 della Facoltà di Scienze MM.FF.NN.
dell’Università degli studi di Salerno. Il gruppo ha utilizzato quindici personal computer in cui è
installato il sistema operativo Windows XP SP3.
In particolare, l’ambiente di lavoro è formato da quattordici host Server (che mettono a
disposizione il servizio di fattorizzazione) e un host Client (che richiede la fattorizzazione di un
numero). La configurazione software include la versione 1.7 della Java Virtual Machine (jdk 1.7).
Il sistema distribuito è stato realizzato utilizzando un’ architettura Client-Server.
Il nostro sistema è simile ad un sistema distribuito collaborativo. L’idea di base è che quando un
Client chiede di fattorizzare un numero, accede a diversi registry (rmiregistry) che contengono i
riferimenti ai servizi di fattorizzazione offerti dai diversi host. Fatto ciò, inizia a distribuire la
computazione ai vari server ottenendo come risultato la lista di sieve. Successivamente costruisce
la matrice con i fattori di base come righe e i sieve risultanti come colonne e tramite il metodo di
eliminazione di Gauss, risolve il sistema e calcola i fattori del numero in input.
La configurazione dei personal computer utilizzati è elencata nella seguente tabella:
DELL OPTILEX 360
Memoria RAM
2,0 GB
Sistema Operativo
Windows XP (5.1) Service Pack 3, x86 32bit
Processore
Intel(R) Pentium(R) Dual CPU E2200 @ 2.20 GHz
Spazio Disco
Spazio Disco disponibile
117 GB
57,6 GB
La seguente tabella mostra i dati tecnici del processore utilizzato. Come si nota si tratta di una
macchina Intel Dual Core.
Intel(R) Pentium(R) Dual CPU E2200 @
2.20 GHz
Launch Date
Q4'07
Processor Number
E2200
# of Cores
2
# of Threads
2
Clock Speed
2.2 GHz
L2 Cache
Bus/Core Ratio
1 MB
11
FSB Speed
800 MHz
FSB Parity
No
Instruction Set
64-bit
Embedded Options Available
No
Supplemental SKU
No
Lithography
65 nm
Max TDP
65 W
VID Voltage Range
0.8500V1.5V
La seguente immagine mostra l’ambiente in cui sono stati eseguiti i test della fattorizzazione.
Caratteristiche Macchine
Memoria: 2,0 GB
Windows XP SP3
Processore 0: Intel(R) Pentium(R) Dual CPU E2200® 2.20
GHz
Processore 1: Intel(R) Pentium(R) Dual CPU E2200® 2.20
GHz
Client
Server 1
Server 2
Server 6
Server 7
Server 8
Server 3
Server 9
Server 14
Server 4
Server 10
Server 13
Server 5
Server 11
Server 12
Figura 4.1 In figura si mostra l’ambiente di testing.
La configurazione software includeva la seguente versione della Java Virtual Machine (JVM):
java version 1.7.0 (Build1320-110325);
platform110131-9c8b3bfb3a1e
Jdk 1.7.0
Per l’esecuzione del programma abbiamo utilizzato degli script Batch di Windows per avviare le
varie classi del progetto.
All’inizio vengono predisposte tutte le macchine Server avviando su ognuna di loro l’rmiregistry e
visto che ogni processore è un dual core, vengono avviati due servizi di fattorizzazione ognuno dei
quali verrà invocato da un thread, così da far lavorare in parallelo due thread su un solo
processore.
Dopo aver fatto ciò, viene specificato il numero da fattorizzare ed eventualmente si configura il
parametro “Dimensione Sieve”. Questo parametro ci permette di specificare la dimensione degli
intervalli dei Sieve che verranno memorizzati nel file generato dal Client. Considerando le
eventuali limitazioni di storage nel laboratorio, abbiamo deciso di non memorizzare nel file tutti i
sieve, ma solo gli intervalli.
Un esempio di file d’input con “Dimensione Sieve = 10” è il seguente:
1,10
11,20
21,30
…
91,97
Dopo aver specificato la configurazione, mandiamo in esecuzione lo script sulla macchina Client,
passandogli come parametro a linea di comando gli indirizzi IP locali delle macchine Server.
Il Client genera il file di input, accede ai registry di ogni macchina e controlla quanti servizi di
fattorizzazione sono disponibili in totale. Questo valore permetterà al Client di dividere il lavoro
per quanti sono i servizi di fattorizzazione a disposizione.
Dopo aver fatto ciò, inizia la fase di divisione del lavoro. In questa fase, il client genera tanti thread
quanti sono i servizi di fattorizzazione a disposizione. Ad ogni thread viene passato come
parametro un oggetto Factorize che effettua il calcolo vero e proprio e un riferimento al server
dove deve essere eseguito. Il client aspetta che tutti i thread finiscono il lavoro e nel frattempo
colleziona tutti i risultati parziali fino ad ottenere la lista finale dei sieve.
La fase successiva è quella della costruzione della matrice con sieve e fattori di base e la
risoluzione del sistema tramite il metodo di eliminazione di Gauss. In questa fase, il client non ha
bisogno dell’aiuto dei server per risolvere il sistema che fornisce in output due soluzioni che
rappresentano i fattori del numero in input. Ovviamente il client controlla che effettivamente le
due soluzioni sono compatibili con il numero in input.
Figura 4.2. Monitor di sistema del Client.
In tutti i test esprimeremo i tempi nel seguente formato: hh:mm:ss,000. Dove hh indica le ore, mm
denota i minuti, ss specifica i secondi e 000 i millisecondi.
4.2 Scelta degli interi da fattorizzare
Conoscendo che l’algoritmo di crittografia RSA adopera due numeri primi per generare l’intero n,
utilizzato come parte della chiave pubblica, abbiamo scelto gli interi da fattorizzare in base a
questo criterio. In particolare, abbiamo scelto due primi e la loro moltiplicazione ci ha dato l’intero
da fattorizzare.
Quindi, dato un intero n, generato dal prodotto di due primi, il nostro algoritmo fattorizza tale
valore restituendo questi due fattori.
Sono stati scelti vari interi da fattorizzare, selezionati in base al numero di bit: da 64 a 138 bit.
64 bit
20
12978441629915362747
211375232977
61400011
Numero
fattori di
base
99
72 bit
22
3544259196660620885771
491420827
7212269000273
140
2744000
80 bit
24
865634683480088490430201
8012217949249
108039333049
196
88 bit
27
163434269570785973006628247
2589512984167
63113902332241 271
96 bit
29
104 bit
32
112 bit
34
120 bit
36
128 bit
39
138 bit
42
4908215835918017317760945596
3
1205608057434690987103195607
8133
3276005466534609097786725547
758259
8178314959310751082646743162
73886383
2873478782520365829619368730
38788889787
2520317529622401748501052493
79065463849553
20938882768666
7
33893367658012
43
69752773008752
833
81170876441092
1989
15612823232949
851171
37801103609661
4700929
23440676802788
9
35570618700372
31
46965953111620
723
10075430151657
86147
18404607159428
252297
66673120331286
8725457
7529536
1990251
1
5024340
9
1235059
92
2940796
25
6814720
00
1548816
893
4181062
131
# bit
# digits
INTERO da Fattorizzare
Fattore 1
Fattore 2
369
498
665
880
1157
1611
Numero
di Sieve
970299
Come si nota dalla tabella, aumentando il numero di bit accrescono i fattori di base e il numero di
sieve da analizzare.
Quando l’algoritmo trova la fattorizzazione, esso controlla se quest’ultima è corretta, nel caso
contrario restituisce un messaggio di errore. In tutti i test compiuti abbiamo sempre riscontrato
una fattorizzazione corretta.
4.3 “Granularità dimensione Sieve”
Il primo test che abbiamo compiuto riguarda la “dimensione Sieve”, cioè abbiamo modificato il
parametro all'interno del programma che indica la dimensione dell’intervallo di sieve presenti in
ogni riga del file d’input generato dal Client. Il test è utile per comprendere orientativamente il
valore migliore per il parametro discusso. Il nostro scopo è registrare il valore di tale parametro
che minimizza il tempo totale reale di esecuzione della fattorizzazione.
In particolare, abbiamo scelto l’intero a 88 bit (163434269570785973006628247), e il numero di
host Server = 14;
La seguente tabella mostra l’esito di questo esperimento. Dimensione Sieve è il parametro
variabile; mentre Real Time indica il tempo totale di esecuzione della fattorizzazione (misurato con
i metodi java StartTime e EndTime); Tempo Divisione Lavoro e Calcolo è il tempo impiegato dal
Client per inviare i task ai Server, collezionare i risultati parziali ottenuti e risolvere il sistema;
Tempo Generazione file di input è il tempo impiegato per la generazione e la scrittura del file di
input.
Numero esperimento Dimensione Sieve
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
5
10
50
100
150
200
250
500
1000
10000
20000
30000
40000
100000
200000
Tempo Divisione Lavoro Tempo Generazione
e Calcolo
file di input
01:20:28,152
01:18:29,230
00:01:58,250
00:42:42,235
00:41:41,987
00:00:59,577
00:12:09,464
00:11:57,323
00:00:11,500
00:08:17,278
00:08:10,840
00:00:05,797
00:07:01,013
00:06:56,279
00:00:04,078
00:06:25,170
00:06:21,576
00:00:02,938
00:06:02,419
00:05:59,326
00:00:02,437
00:05:14,326
00:05:12,326
00:00:01,328
00:04:41,263
00:04:39,905
00:00:00,640
00:03:21,484
00:03:20,249
00:00:00,109
00:03:13,248
00:03:11,498
00:00:00,063
00:03:36,253
00:03:34,143
00:00:00,047
00:08:45,659
00:08:43,018
00:00:00,046
Error: Java heap space
00:00:00,032
Error: Java heap space
00:00:00,015
Real Time
Dalla tabella si nota che gli ultimi due esperimenti non sono andati a buon fine, perché ogni riga da
costruire era molto grande, quindi ha determinato un eccessivo utilizzo dell’heap space di java.
Figura 4.3 Nel grafico si mostra il tempo totale della fattorizzazione a variare del parametro
“Dimensione Sieve”.
Dall’esperimento presentato si evince che il tempo totale, in questo caso, si minimizza quando il
parametro in esame è settato a 20000. Aumentando il valore di tale parametro, oltre il valore
20000, il tempo totale degenera a causa di più dati da trasferire e gestire.
4.4 Scalabilità
In questo esperimento mostreremo come aumentando il numero di host (Server) il tempo totale
di esecuzione diminuisce. In particolare abbiamo preso due interi, uno a 96 bit e l’altro a 104 bit.
Abbiamo scelto tali numeri, poichè aumentando il numero di bit e riducendo il numero di host, il
tempo totale diventava inaccettabile per l’esecuzione complessiva dei test.
In questo esperimento abbiamo utilizzato i seguenti parametri fissati:
ο‚·
ο‚·
Dimensione Sieve = 20000;
Numero massimo di host Server = 14;
La seguente tabella mostra i tempi di esecuzione della fattorizzazione dell’intero a 96 bit
(49082158359180173177609455963). In particolare, si mostra il tempo reale totale di esecuzione,
il tempo di generazione e scrittura del file d’input originato dal Client (tempo misurato in java con
la funzione System.getTimeInMillis()) e il tempo di Divisione Lavoro e Calcolo ottenuto anch’esso
attraverso la precedente funzione java.
Numero
Host
Server
1
2
4
8
11
14
Numero
Processi
Server
2
4
8
16
22
28
Real Time
Tempo Divisione
Lavoro e Calcolo
00:17:59,196
00:12:43,276
00:10:10,137
00:08:50,075
00:08:37,231
00:08:14,147
00:17:58,415
00:12:42,432
00:10:09,089
00:08:48,668
00:08:35,544
00:08:12,193
Tempo
Generazione
File di input
00:00:00,078
00:00:00,062
00:00:00,063
00:00:00,078
00:00:00,110
00:00:00,078
La seguente tabella mostra i tempi di esecuzione della fattorizzazione dell’intero a 104 bit
(12056080574346909871031956078133). In particolare, si mostra il tempo reale totale di
esecuzione, il tempo di generazione e scrittura del file d’input originato dal Client (tempo misurato
in java con la funzione System.getTimeInMillis()) e il tempo di Divisione Lavoro e Calcolo ottenuto
anch’esso attraverso la precedente funzione java.
Numero
Host
Server
1
2
4
8
11
14
Numero
Processi
Server
2
4
8
16
22
28
Real Time
00:58:42,003
00:38:34,823
00:29:20,379
00:24:27,469
00:23:02,788
00:21:50,496
Tempo
Tempo
Generazione
Divisione Lavoro e Calcolo
File di input
00:58:40,722
00:00:00,218
00:38:33,465
00:00:00,218
00:29:18,848
00:00:00,219
00:24:25,673
00:00:00,219
00:23:00,709
00:00:00,141
00:21:48,199
00:00:00,156
Figura 4.4 In figura, si mostra il Real Time della fattorizzazione dei due interi in esame al variare del
numero di host Server.
Come si nota, sia dal grafico sia dalle tabelle, il tempo totale (Real Time) di esecuzione decresce
all’aumentare del numero di macchine a disposizione. Questa tendenza è maggiormente visibile
quando dimezziamo il numero di macchine e utilizziamo un numero con più bit, poiché il file
d’input è più grande a causa dell’elevato numero di sieve.
Precisiamo che aumentando il numero di bit dell’intero fa fattorizzare, il numero di sieve da
analizzare cresce esponenzialmente, quindi occorre più tempo per eseguire l’algoritmo.
4.5 Numero di bit degli interi da fattorizzare
Il seguente test ci permette di analizzare come cresce il tempo di fattorizzazione al crescere della
dimensione in bit dell'intero da fattorizzare.
Per il seguente test abbiamo adoperato alcuni interi descritti nella sezione 4.2. Inoltre, sono stati
utilizzati i seguenti parametri:
ο‚·
ο‚·
Dimensione Sieve = 20000;
Numero di host Server = 14;
Numero
Numero bit
esperimento
1
2
3
4
5
6
7
8
9
64 bit
72 bit
80 bit
88 bit
96 bit
104 bit
112 bit
128 bit
138 bit
Real Time
Tempo
Divisione Lavoro e Calcolo
00:00:09,938
00:00:26,312
00:01:11,796
00:03:12,964
00:08:14,147
00:21:50,496
01:17:37,114
07:02:48,738
28:41:21,207
00:00:08,656
00:00:24,890
00:01:10,265
00:03:11,276
00:08:12,193
00:21:48,199
01:17:33,958
07:02:41,254
28:41:03,598
Tempo
Generazione
File di input
00:00:00,016
00:00:00,016
00:00:00,047
00:00:00,063
00:00:00,078
00:00:00,156
00:00:00,328
00:00:01,047
00:00:06,094
Real Time al variare del numero di bit
01.04.48,000
00.58.18,101
00.57.36,000
00.50.24,000
Real Time
00.43.12,000
00.36.00,000
00.28.48,000
RT
00.19.33,253
00.21.36,000
00.14.24,000
00.07.41,243
00.07.12,000
00.02.59,769
00.01.38,432
00.01.04,382
00.00.47,293
00.00.00,000
0
1
2
3
4
5
6
7
8
Numero esperimento
Figura 4.5 Nel grafico, si mostrano i tempi totali di esecuzione della fattorizzazione al variare del
numero di bit.
Come ci aspettiamo da un algoritmo esponenziale, al crescere della dimensione dell’input (numero
di bit dell’intero da fattorizzare) il tempo di esecuzione totale aumenta radicalmente. Nel nostro
caso, i tempi aumentano perché il numero di sieve cresce esponenzialmente, come si nota dal
grafico sottostante.
Figura 4.6 Nel grafico, si mostra come aumenta il numero di sieve da analizzare, estendendo il
numero di bit del numero da fattorizzare.
5 Conclusioni
L’obiettivo del progetto è: “fattorizzare un intero adoperando il framework Java RMI ”.
Abbiamo scelto di implementare il progetto con Java RMI per sfruttare la sua capacità di invocare
metodi remoti, utilizzando inoltre i thread di Java per distribuire il lavoro tra più macchine Server.
Dai test effettuati si evince che l’implementazione realizzata riesce a fattorizzare un numero
sfruttando la potenza computazionale delle macchine messe a disposizione, ma è facile intuire che
con granularità molto piccola (5-10) o molto grande (30000-40000) del parametro “Dimensione
Sieve”, si ha un degrado progressivo delle prestazioni in termini di tempo impiegato per trovare i
fattori primi del numero. Questo perché con granularità molto piccola il Client genera un file molto
grande in termini di linee di testo, cioè costituito da intervalli di sieve molto piccoli per cui ogni
macchina Server esegue poco calcolo computazionale ma molte volte. Al contrario, con granularità
molto grande viene generato un file con poche linee di testo, ognuna delle quali è costituita da
intervalli di sieve molto grandi e tutto ciò comporta un notevole sforzo computazionale da parte
dei Server.
Come risultato complessivo siamo riusciti a fattorizzare un intero a 138 bit in circa 28 ore su 14
host Server.