Final report for a university project assignment at the department of Artificial Intelligence (UNICT). The document is about performance evaluation of an algorithm written in Microsoft .NET F# implementing the A* algorithm. The algorithm is intended to be used for finding shortest paths on city maps given a start and a finish position. Performance comparison is performed with an existing algorithm performing exhausting-naive graph search written in Prolog.
Microsoft .NET F# Implementation of A* search algorithm
1. RouteSharp PathFinder: Un’applicazione Microsoft F# per
la ricerca di cammini minimi in una rete complessa
mediante l’algoritmo di ricerca A*
Davide G. Monaco, Andrea Tino
Prof. Ing. A. Faro @ DIIEI UNICT
Marzo 2011
1 Introduzione generale
RouteSharp ` un’applicazione Microsoft .NET per l’acquisizione di reti complesse e la
e
ricerca di cammini minimi su esse. L’intera applicazione si divide in due grandi unit`:
a
1. PathFinder Si tratta del core di sistema scritto in F# in grado di caricare reti
complesse da file (tramite un microlinguaggio usato per la specifica dei nodi, delle
connessioni e delle query) e fornire API per la ricerca dei cammini minimi.
2. RouteSharp Si tratta di tutta l’applicazione comprensiva del core in F# e dei
futuri moduli di interfaccia scritti in C#.
1.1 Funzionalit` implementate
a
L’applicazione attualmente sviluppata consta del solo core in F# che mette a dispo-
sizione le seguenti funzionalit`:
a
1. Definizione delle reti: E’ possibile scrivere da file la rete complessa da cari-
care. I file (con estensione .rs denominati in maniera pi` specifica netfile) vengono
u
scritti tramite un microlinguaggio line-based e permettono di definire i nodi e le
connessioni della rete. E’ inoltre possibile definire le query di ricerca negli stessi
netfile.
2. Caricamento e traduzione: I file vengono caricati dall’applicazione e tradotti
in strutture interne che concorreranno alla generazione dei nodi e del loro vicinato.
3. Net traversal: E’ possibile, dalle query definite, effettuare la ricerca di cammini
minimi all’interno della rete caricata. La ricarca avviene tramite algoritmo A*.
4. Modalit` di interazione: L’applicazione, il core in F#, dialoga tramite riga
a
di comando. E’ possibile specificare diverse opzioni in esecuzione tra le quali
anche una modalit` interattiva mediate la quale il netfile viene esaminato, le query
a
ignorate, e la rete viene dunque costruita e all’utente viene infine richiesto di
definire, di volta in volta, lo start point e l’end point del percorso da cercare.
1
2. 2 Introduzione al paradigma funzionale
La programmazione funzionale ` un paradigma di programmazione in cui la computazione
e
viene trattata come la valutazione di funzioni matematiche, evitando l’uso stati e dati
mutabili. Le origini della programmazione funzionale possono essere ricondotte al lambda
calcolo ed alla ricorsione.
La programmazione funzionale pone maggior accento sulla definizione di funzioni,
rispetto ai paradigmi procedurali e imperativi, che prediligono la specifica di una se-
quenza di comandi da eseguire. In questi ultimi, i valori vengono calcolati cambiando
lo stato del programma attraverso delle assegnazioni; un programma funzionale ` im-
e
mutabile: i valori non vengono trovati cambiando lo stato del programma, ma costruendo
nuovi stati a partire dai precedenti.
La differenza tra il concetto di funzione in un linguaggio imperativo ed in un linguag-
gio funzionale consiste nel fatto che nel primo una funzione pu` avere dei side-effects (o
o
effetti collaterali), mentre nel secondo no. Ci` rappresenta uno dei maggiori vantaggi
o
nell’uso del paradigma funzionale, in quanto non solo sar` molto pi` semplice verifi-
a u
care la correttezza e la mancanza di bug del programma, ma sar` anche pi` efficace
a u
l’ottimizzazione dello stesso.
Impiego
Tipicamente, come tutti i linguaggi di alto livello, i linguaggi funzionali sono meno effi-
cienti in merito all’uso di CPU e memoria rispetto a linguaggi imperativi di pi` basso
u
livello come il C. Ad ogni modo ` possibile affermare che per programmi che effettuano
e
computazioni numeriche intense, gestiscono grosse matrici o database multidimension-
ali, alcuni linguaggi funzionali riescono a raggiungere le prestazioni di poco inferiori al
C. Inoltre, l’immutabilit` dei dati, in molti casi, aumenta l’efficienza dell’esecuzione,
a
permettendo al compilatore di fare assunzioni che sarebbero poco sicure nel caso di lin-
guaggio imperativo.
Si potrebbe quindi pensare che i linguaggi funzionali possano essere impiegati in
modo efficace solamente per scopi puramente accademici.
Molti linguaggi funzionali, invece, sono stati utilizzati per scopi commerciali o indus-
triali negli ultimi decenni, basti pensare ad Erlang che venne impiegato negli anni 80
dalla Ericsson per implementare sistemi di telecomunicazioni fault-tolerant, oppure F#,
che essendo incluso nell’ambiente .NET della Microsoft sta trovando impiego in ambito
commerciale.
La decisione di implementare l’algoritmo A* in F# ` scaturita dalla volont` di ri-
e a
considerare la programmazione funzionale e proporre un’alternativa non presente in
letteratura.
3 L’algoritmo A*
A* (A star) ` un noto algoritmo per la ricerca su grafi che individua un percorso da
e
un nodo iniziale ad un nodo destinazione, descritto per la prima volta nel 1968 come
2
3. estenzione dell’algoritmo di Dijkstra.
3.1 Introduzione
A* utilizza ricerche di tipo best-first per trovare il percorso a costo minore; inoltre,
utilizzando una funzione euristica di tipo distance-plus-cost, riesce ad essere molto
performante e a determinare l’ordine in cui i nodi verranno esplorati. Tale funzione, che
chiameremo f (x), viene determinata secondo la relazione:
f (x) = g(x) + h(x)
dove:
- g(x) ` una funzione che tiene conto del costo di spostamento all’interno del grafo
e
dal nodo di partenza al nodo corrente.
- h(x) ` una funzione di stima euristica che calcola la distanza dal nodo corrente al
e
nodo destinazione. Deve essere un’euristica ammissibile, ovvero una funzione che
mai sovrastima il costo del raggiungimento della destinazione. Per questo motivo,
in genere, soprattutto in ambito routing, la distanza considerata ` una distanza
e
”in linea retta” (es. la norma in uno spazio vettoriale).
3.2 Principio
A* attraversa il grafo verso la destinazione seguendo il percorso a minor costo conosciuto,
mantenendo ordinata una priority queue di segmenti alternativi lungo la strada. Se,
quindi, in un qualsiasi punto un possibile percorso ha un costo superiore rispetto ad
un altro gi` incontrato precedentemente, l’algortmo abbandona il percorso a costo pi`
a u
alto in favore del segmento a costo pi` basso. Questo processo viene applicato finch´ la
u e
destinazione non viene raggiunta. Come accennato precedentemente, il costo preso in
esame non ` costituito unicamente dal costo del singolo spostamento dal nodo corrente
e
al nodo vicino, bens` considera tutti gli spostamenti effettuati fino all’attuale posizione,
ı
g(x), e la distanza in linea retta dalla destinazione, h(x), potendo quindi valutare quanto
effettivamente ci si avvicina alla destinazione con il successivo spostamento.
3.3 Funzionamento
Iniziando dal nodo di partenza, come precedentemente accennato, viene mantenuta una
priority queue, nota convenzionalmente come open set. Il nodo con f-score, valore
di f (x), minore all’interno della coda ha maggiore priorit`. Ad ogni step, il nodo con
a
priorit` pi` alta viene rimosso dall’open set, vengono calcolati ed annotati gli f-score dei
a u
suoi vicini e gli stessi vengono aggiunti all’open set.
L’algoritmo continua finch´ il nodo di destinazione ha un f-score minore di qualsiasi
e
altro nodo nell’open set, ritornando il percorso con successo, o altrimenti finch´ non vi
e
sono pi` nodi nell’open set, nel qual caso il percorso non esiste.
u
3.4 Propriet`
a
A* ` un algoritmo completo, quindi siamo sicuri che trover` sempre una soluzione, se
e a
questa esiste. Inoltre, esso ` sia ammissibile che ottimo rispetto agli altri algoritmi
e
3
4. di ricerca ammissibili: ha una stima ottimistica del costo del percorso attraverso ogni
nodo considerato. L’ottimismo consiste anche nel sapere che il vero costo del percorso
attraverso ciascun nodo verso la destinazione varr` almeno quanto vale la nostra stima.
a
Quindi la conoscenza di A* ` cruciale. Per definizione, quando l’algoritmo ha terminato
e
la sua ricerca avr` trovato un percorso il cui costo attuale ` pi` basso del costo stimato
a e u
per ogni percorso attraverso tutti i nodi rimasti nell’open set, essendo sicuri di non aver
trascurato alcun percorso dal costo minore, quindi A* ` ammissibile.
e
Se viene utilizzato un closed set per tener traccia dei nodi gi` esaminati, incremen-
a
tando le prestazioni dell’algoritmo, la funzione euristica h(x) oltre che ammissibile deve
essere monotona, ovvero deve soddisfare la relazione:
h(x) ≤ d(x, y) + h(y)
dove d(x, y) ` la distanza tra i nodi x e y.
e
3.5 Complessit`
a
La complessit` di A* dipende dalla funzione euristica utilizzata.
a
Nel caso peggiore, il numero di nodi espansi ` esponenziale rispetto alla lunghezza della
e
soluzione, ma ` polinomiale quando lo spazio di ricerca ` un albero, la destinazione ` un
e e e
singolo nodo e la funzione euristica h(x) soddisfa la seguente relazione:
|h(x) − h∗ (x)| = O(log(h∗ (x)))
dove h∗ (x) ` l’euristica ottima, ovvero la reale distanza che intercorre tra il nodo desti-
e
nazione e il nodo x; detto in altri termini, se l’errore della funzione h(x) non cresce pi`
u
velocemente del logaritmo dell’euristica ottima h∗ (x).
4 A* in RouteSharp
In RouteSharp l’algoritmo A* ` stato inizialmente prototipato in Perl e successivamente
e
implementato in F#.
4.1 Implementazione
Essendo F# un linguaggio multiparadigma focalizzato sulla programmazione funzionale,
in RouteSharp l’implementazione dell’algoritmo differisce dalla canonica, presentata in
letteratura con approccio procedurale. Sono state implementate diverse funzioni, la
maggior parte delle quali ricorsive, al fine di ricoprire tutti gli aspetti implicati nel cor-
retto funzionamento dell’algoritmo.
Di seguito viene riportato il codice delle funzioni implementate, AStar(), Scan(),
Update(), RebuildPath() e PrintPath(), accompagnato da commenti esplicativi per
ciascuna funzione.
type public Cost = double
type public StarNode = {
Node : NetworkNode ;
FCost : Cost ;
GCost : Cost ;
4
5. HCost : Cost ;
Parent : NetworkNode option ;
}
Sono stati definiti i tipi Cost, mappato sul tipo base double, e StarNode, che in-
capsula il nodo corrente nella rete, Node di tipo NetworkNode, e associandogli i valori
di f (x), g(x) e h(x) di cui l’algoritmo ha bisogno. Viene inoltre tenuta traccia del nodo
di provenienza, Parent per poter ricostruire il percorso una volta trovato. La keyword
option denota che il campo ` opzionale e che quindi potrebbe non essere presente.
e
let public AStar start goal =
let norm_start = Norm ( start , goal )
let start1 = {
Node = start ;
FCost = norm_start ;
GCost = 0.0;
HCost = norm_start ;
Parent = None
}
let goal1 = {
Node = goal ;
FCost = 0.0;
GCost = 0.0;
HCost = 0.0;
Parent = None
}
let ol = [ start1 ]
let cl = [ ]
Scan ol cl goal1 Map . empty
La funzione AStar() accetta in ingresso due nodi, start e goal, nel nostro caso
saranno sempre NetworkNode, calcola la distanza in linea retta tra essi, tramite la fun-
zione Norm(), e ne inizializza i relativi StarNode associati, start1 e goal1. Infine,
dopo aver inizializzato le due liste ol e cl, rispettivamente open list e closed list, e viene
invocata la funzione Scan(), passando come parametri la open list, la closed list, lo
StarNode associato al nodo destinazione ed una mappa vuota.
let rec public Scan ol cl goal come_from =
match ol with
| [] -> ( cl , come_from )
| h :: ol_tail ->
if h . Node . Name = goal . Node . Name then
( cl , come_from )
else
let ol2 = ol_tail
let cl2 = h :: cl
let starnodes = List . map ( fun ( x : N e t w o r k N o d e N e i g h b o u r ) -> {
StarNode . Node = x . Node ;
StarNode . FCost = h . GCost + x . Weight ;
StarNode . GCost = h . GCost + x . Weight ;
5
6. StarNode . HCost = 0.0;
StarNode . Parent = None
}) h . Node . Neighbourhood
let ( ol3 , cl3 , come_from2 ) = Update ol2 cl2 starnodes h come_from
Scan ol3 cl3 goal come_from2
La funzione Scan() ` una funzione ricorsiva che accetta in ingresso due liste ol e
e
cl, rispettivamente la open e la closed, il nodo destinazione, goal, ed una mappa che
permette di stabilire i rapporti di parentela tra i nodi, come from.
Se ol, ` una lista vuota, [], viene ritornata una tupla contenente la closed list e la
e
mappa di nodi, (cl, come from). In caso contrario, viene estratto il nodo in testa
dalla open list, h, ovvero il nodo con f-score migliore, e vengono effettuati dei controlli.
Se h corrisponde al nodo di destinazione, viene ritornata la tupla contenente la closed
list e la mappa di nodi; in caso contrario, h viene inserito in closed list, affinch´ si abbia
e
memoria del fatto che tale nodo ` stato esaminato, e viene creata una lista contenente
e
i suoi vicini; per aggiornare open list, closed list e mappa di nodi, viene chiamata la
funzione Update(), che torner` una tupla contenente i tre elementi richiesti, ed infine
a
viene effettuata la chiamata ricorsiva a Scan().
La funzione Update() ` un po’ pi` complessa, quindi, per semplificarne la spie-
e u
gazione, verr` illustrata passo passo.
a
let rec public Update ol cl neigh wn come_from =
match neigh with
| [] -> ( ol , cl , come_from )
| h :: neigh_tail ->
[...]
Update() ` una funzione ricorsiva che ritorna una tupla contenente la open list, la
e
closed list e la mappa di nodi aggiornata in base alle informazioni passate.
Accetta in ingresso ol e cl, rispettivamente open e closed list, wn, il nodo corrente,
neigh, la lista dei vicini del nodo corrente, e come from, la mappa di nodi gi` descritta
a
precedentemente.
Se la lista dei vicini ` vuota, la funzione ritorna la tupla, altrimenti prosegue esaminando
e
il primo vicino in neigh che chiameremo h.
if List . exists ( fun x -> x . Node . Name = h . Node . Name ) cl then
Update ol cl neigh_tail wn come_from
else
*(1)
Se il nodo h ` presente in closed list passa al vicino successivo chiamando ricorsiva-
e
mente Update(), altrimenti prosegui esaminando h.
-*(1) -
let testG = wn . GCost + ( wn . Node . G e t W e i g h t T o N e i g h b o u r ( h . Node . Name ))
if List . exists ( fun x -> x . Node . Name = h . Node . Name ) ol then
*(2)
else
let h2 = {
Node = h . Node ;
6
7. FCost = testG ;
GCost = testG ;
HCost = h . HCost ;
Parent = Some wn . Node
}
let ol2 = h2 :: ol
let ol3 = List . sortBy ( fun x -> x . FCost ) ol2
let come_from2 = Map . add h . Node . Name wn . Node . Name come_from
Update ol3 cl neigh_tail wn come_from2
Calcola il valore di g(x), testG, sul nodo h.
Se h ` presente in open list prosegui valutando ulteriori casi, altrimenti incapsula h in
e
uno StarNode e aggiungilo alla open list. Ordina la open list per f-score e aggiorna
come from, ponendo wn come predecessore di h. Infine invoca ricorsivamente Update
con la nuova open list e lista dei nodi.
-*(2) -
let comp_node = List . find ( fun x -> x . Node . Name = h . Node . Name ) ol
if testG < comp_node . GCost then
let h2 = {
Node = h . Node ;
FCost = testG (* h . FCost *);
GCost = testG (* h . GCost *);
HCost = h . HCost ;
Parent = Some wn . Node
}
let replacer ( x ) =
if x . Node . Name = h . Node . Name then
h2
else
x
let ol2 = List . map ( fun x -> ( replacer x )) ol
let ol3 = List . sortBy ( fun x -> x . FCost ) ol2
let come_from2 = Map . add h . Node . Name wn . Node . Name come_from
Update ol3 cl neigh_tail wn come_from2
else
let h2 = {
Node = h . Node ;
FCost = testG (* h . FCost *);
GCost = testG (* h . GCost *);
HCost = h . HCost ;
Parent = h . Parent
}
let replacer ( x ) =
if x . Node . Name = h . Node . Name then
h2
else
7
8. x
let ol2 = List . map ( fun x -> ( replacer x )) ol
let ol3 = List . sortBy ( fun x -> x . FCost ) ol2
Update ol3 cl neigh_tail wn come_from
In questo caso abbiamo trovato h nella open list. Confrontiamo il valore di g(x)
precedentemente annotato con testG. Se il valore di test ` minore, aggiorna i valori
e
in ol e come from, riordina la open list e invoca ricorsivamente Update(); altrimenti
aggiorna solamente come from e invoca sempre Update().
let rec public RebuildPath finalpath nodemap node =
let init lst =
match lst with
| [] -> [ node ]
| _ -> lst
try
let par = Map . find node nodemap
RebuildPath ( par :: ( init finalpath )) nodemap par
with
| :? K e y N o t F o u n d E x c e p t i o n -> finalpath
La funzione RebuildPath() ` una funzione ricorsiva che ritorna il percorso.
e
Accetta in ingresso il percorso da ritornare, finalpath, la mappa di nodi elaborata
durante il processamento di A*, nodemap, e il nodo corrente, node.
La ricostruzione viene fatta risalendo dalla destinazione al nodo di partenza, valutando
il nodo predecessore annotato nella nodemap.
Viene sollevata un’eccezione nel caso in cui il percorso non ` stato trovato.
e
5 Analisi comparativa con un’applicazione Prolog: PathFinder.exe vs
Traffic.exe
Per determinare una scala di performance e un ordine di efficienza di A* scritto in F#,
` stata condotta una serie di esecuzioni diagnostiche (run delle applicazioni in contesti
e
controllati) per accertare le attivit` svolte da PathFinder nel caricare la rete e nel cercare
a
il cammino minimo. Le stesse attivit` sono state condotte su Traffic, un’applicazione
a
scritta in Prolog avente gli stessi obiettivi di PathFinder.
5.1 Perch` questa analisi
e
La possibilit` di esaminare le prestazioni di PathFinder tramite un’analisi comparata
a
con un’applicazione Prolog permette di stabilire un’ordine di grandezza circa le perfor-
mance di una’aplicazione scritta tramite un linguaggio multiparadigma (OO + Func-
tional) e un’applicazione scritta tramite un linguaggio logico. Considerando inoltre che
A* possiede, attualmente, pochissime implementazioni funzionali e multiparadigma in
letteratura, questa analisi mette anche a fuoco potenziali sviluppi di F# nel campo delle
reti complesse.
5.1.1 Limiti e considerazioni
Malgrado si tratti di un’analisi comparata condotta mediante rigidi schemi e col maggior
rigore possibile; ` necessario puntualizzare che i run controllati e i dati raccolti non
e
8
9. possono essere considerati per la definizione di una precisa tabella delle performance
(dalla quale ` possibile stabilire, con certezza, quale sia l’applicazione migliore per una
e
certa caratteristica in esame) a causa dei seguenti motivi:
1. Numero di run: Il numero di esecuzioni controllate non ` tale da stabilire una
e
corretta descrizione generale del comportamento delle due applicazioni.
2. Contesto software: Il contesto software in cui le applicazioni sono state eseguite
non rispecchiava un classico scenario di esame. Ovvero la presenza di processi in
background di sistema e di altre applicazioni ha inquinato l’ambiente di lavoro e
ha, pertanto, reso le grandezze in esame, dipendenti da parametri rumorosi.
3. Contesto hardware: La macchina su cui sono state condotte le esecuzioni non
rispecchiava una vera e propria macchina per test controllati.
Per questo motivo si considerino i risultati seguenti come il frutto di un’analisi volta a
determinare le dinamiche generali delle due applicazioni.
5.2 Strumentazione
Al fine di ottenere dati precisi per analisi in profondit` delle performance delle due
a
applicazioni, coerentemente al contesto software di base (sistema operativo), ` stata
e
utilizzata un’applicazione Microsoft per il profiling di una sessione di lavoro. Microsoft
Windows Performance Analysis (WPA) ` stata scelta come tool principale per l’analisi
e
delle prestazioni sopratutto a fronte della sua perfetta integrazione coi sistemi Windows
(il set di tool interagisce perfettamente con le funzionalit` di sistema, rendendo la suite
a
un componente nativo di Windows). Il risultato ` la possibilit` di ottenere informazioni
e a
davvero precise circa l’esecuzione di una qualsiasi applicazione.
5.2.1 Algoritmo di generazione delle reti complesse
Per generare le reti complesse utilizzate nelle sessioni di run, ` stato utilizzato l’algoritmo
e
di Watts e Strogatz.
5.3 Sessione di esecuzione a reti dimensione-variata
Questa sessione di esecuzioni ha visto i due programmi competere su reti a dimensione
variabile. Lo scopo ` quello di verificare l’andamento delle prestazioni al crescere del
e
numero dei nodi, mantenendo costante, nei limiti del possibile, la struttura di vicinato
della rete.
5.3.1 Reti caricate
Le reti caricate dai programmi rispecchiano in tutto 11 configurazioni complesse con un
numero di nodi variabile da 50 a 8000. Tramite l’applicazione di uno stress considirevole,
al crescere del numero dei nodi, si verifica la risposta delle applicazioni all’avanzamento
di tale complessit`.
a
5.3.2 Risposta dei tempi di esecuzione
I tempi di esecuzione hanno avuto due differenti andamenti:
9
10. • PathFinder: L’applicazione ha mostrato un andamento crescente dei tempi. La
dilatazione temporale evidenziata da PathFinder mette in mostra una dipendenza
coerente tra numero di nodi e tempi. La crescita diviene considerevole a partire
dai 1000 nodi in su, dove il salto in ordine di grandezza determina una netta
spaccatura nella complessit` dele operazioni da eseguire. Tuttavia, l’applicazione
a
risponde bene ed in maniera controllata e predicibile.
• Traffic: L’applicazione mostra un pattern fortemente irregolare nei tempi di es-
ecuzione al crescere del numero dei nodi. Sono presenti inoltre forti sbalzi al
crescere del numero dei nodi. La rete ad 8000 nodi non viene caricata al completo
e l’applicazione sperimanta un crash non occasionale (run multipli sulla configu-
razione a 8000 nodi mostrano che Traffic interrompe l’esecuzione a seguito dello
stesso crash).
5.3.3 Risposta dei tempi di IO
I tempi di IO (vengono presi in esame solamente i file di condifugrazione della rete per
i due programmi) hanno avuto due differenti andamenti:
• PathFinder: L’applicazione ha mostrato un andamento crescente a tratti dei
tempi. La crescita dei tempi di IO, tuttavia, in rapporto ai tempi di computazione,
avviene in maniera differente, mettendo in luce la controparte relativa ai tempi di
ricerca del percorso ottimo (tempo di pura computazione). I dati mostrano, infatti,
un andamento crescente delle attivit` di IO che, per`, riscontrano significativi in-
a o
nalzamenti solamente nella transizione da certi numeri di nodi. Dunque la crescita
` si presente, ma non costante, tuttavia regolare. Da notare, il raggiungimento dei
e
7000 e 8000 nodi dove un brusco innalzamento viene rilevato. Si tratta, ed ` bene
e
sottolinearlo, di un pattern comunque regolare, infatti la curva dei tempi mostra
variazioni d’ordine e tratti di non variazione d’ordine susseguirsi regolarmente.
• Traffic: L’applicazione mostra un pattern fortemente irregolare nei tempi di IO
al crescere del numero dei nodi. La crescita si mantiene piuttosto regolare fino al
raggiungimento dei 1000 nodi, oltre i quali vengono sperimentati sbalzi acuti dei
tempi. Tale comportamento evidenzia molte irregolarit` nell’esecuzione.
a
5.3.4 Rapporto dei tempi di computazione ed IO
I tempi di IO e i tempi di pura computazione determinano una percentuale sui tempi
di esecizione totale per ambedue i programmi. L’esame di tali rapporti mette in luce
il comportamento delle due applicazioni circa le loro attivit` con il filesystem dandoci
a
la possibilit` di valutare quante parte dell’esecuzione viene materialmente spesa nelle
a
operazioni sui file di configrazione:
• PathFinder: L’applicazione ha mostrato un andamento quasi costante del rap-
porto computazione/IO. Osservando l’andamento complessivo al crescere del nu-
mero dei nodi, si nota come l’applicazione comunque mantenga sempre una per-
centuale al di sotto del 25% dedicata alla computazione vera e propria, mentre un
complementare 75% circa viene sempre dedicato alla gestione delle operazioni su
file. Tale risultato mette in luce la propriet` pi` importante di PathFinder: la
a u
lentezza crescente, al crescere della rete, ` dovuta nettamente ai tempi necessari
e
affinch` la rete possa essere caricata e tradotta.
e
10
11. • Traffic: L’applicazione mette in luce una percentuale estremamente a favore dei
tempi di computazione, segno del fatto che i tempi di accesso e modifica dei file di
configurazione, constano di un parte davvero minima nell’esecuzione complessiva.
5.4 Sessione di esecuzione a reti struttura-variata
Questa sessione di esecuzioni ha visto i due programmi competere su reti generate sec-
ondo le seguenti distribuzioni:
• Watts e Strogatz: La rete viene generata a partire da una topologia di base e il
processo itera i vari nodi generando nuove connessioni con una data probabilit`.
a
• Barabasi: La rete viene generata a partire da una rete triangolare. Alla rete
vengono dunque aggiunti i vari nodi con un dato numero di connessioni. La prob-
abilit` che tali connessioni vengano agganciate ai vari nodi ` proporzionale al loro
a e
vicinato.
• Bernoulli: La rete viene generata a partire da una rete di base con tutti i nodi. Le
connessioni vengono create secondo lo schema stocastico a tentativi di Bernoulli.
Lo scopo ` quello di verificare l’andamento delle prestazioni al crescere del connession-
e
ismo per le varie tipologie di reti.
5.4.1 Reti caricate
Le reti caricate dai programmi rispecchiano in tutto 10 configurazioni complesse con un
numero di nodi fissato a 30. Tramite l’applicazione di uno stress considirevole, al crescere
del connessionismo delle reti, si verifica la risposta delle applicazioni all’avanzamento di
tale complessit`.
a
5.4.2 Risultati in generale
I run hanno messo in luce un comportamento molto statico dei tempi di esecuzione
da parte di Traffic, mentre una maggiore sensibilit` viene riscontrata da PathFinder.
a
Riguardo le percentuali di effettiva computazione ed IO, troviamo sempre una grande
disparit` tra le due applicazioni, dove Traffic utilizza sempre non meno del 25% del
a
tempo totale, in operazioni di IO.
5.5 Sessione di esecuzione a reti heuristic-aware
Questa sessione di esecuzioni ha visto i due programmi competere su reti spazialmente
collocate all’interno di un sistema di riferimento bidimensionale. I nodi hanno coordi-
nate e lo spazio normato in esame viene considerato per trovare, appunto nella norma
euclidea, l’euristica utilizzabile da A*. In tale contesto si vuole misurare l’efficienza di
A* nel raggiungere l’obiettivo quanto prima mediante l’uso delle euristiche.
5.5.1 Risultati in generale
Un notevole abbattimento dei tempi di computazione da parte di A* viene riscontrato
grazie all’attivazione del meccanismo ad euristica. I tempi di PathFinder si riducono
notevolmente e la forbice con Traffic si allarga. Traffic rimane pi` lento, considerando il
u
puro tempo di computazione (eliminando l’IO complessivo), PathFinder riesce a trovare
il cammino minimo in pochi millisecondi.
11