Εργασία 1η Παράλληλα & Διανεμημένα Συστήματα Μόσχογλου Στυλιανός 6978 Αριστοτέλης Μικρόπουλος 6977 28 Νοεμβρίου 2011 Περιεχόμενα 1 Πρόλογος 2 2 Ο σειριακός k nearest neighbor algorithm 2 2.1 Η λειτουργία του knntest.c.................... 2 2.2 Η λειτουργία του knns.c...................... 3 3 Ιδέες παραλληλοποίησης 4 3.1 Διαχωρίζοντας το dataset..................... 4 3.2 Διαχωρίζοντας τα queries..................... 4 3.2.1 Η απλή εκδοχή...................... 4 3.2.2 Η πιο σύνθετη, αλλά αποδοτικότερη........... 5 4 Ο αλγόριθμος που χρησιμοποιήσαμε 5 4.1 Ανάλυση του threading..................... 6 4.2 Η απόδοσή του σε γράφημα.................... 7 4.2.1 Στον Διάδη........................ 7 4.2.2 Σε οικιακό μηχάνημα................... 9 5 Επίλογος 10 1
1 Πρόλογος Η εργασία αυτή πραγματοποιήθηκε στα πλαίσια του μαθήματος Παράλληλα & Διανεμημένα Συστήματα, στο πολυτεχνικό τμήμα Ηλεκτρολόγων Μηχανικών και Μηχανικών Υπολογιστών του Αριστοτελείου Πανεπιστημίου Θεσσαλονίκης, υπό την επίβλεψη του καθηγητού Πιτσιάνη Νικόλαου. Είναι δακτυλογραφημένη σε L A TEX 2ε. Σκοπός της είναι να περιγράψει το κατά δυνατόν καλύτερα την χρήση και παραλληλοποίηση του k nearest neighbor algorithm. Ο κώδικας που χρησιμοποιήθηκε, μαζί με σχόλια όπου κρίνεται απαραίτητο, βρίσκεται στα αρχεία knns.c, knns.h και knntest.c, επισυναπτόμενα στο τρέχον rar αρχείο. Ακολουθεί ενδελεχής περιγραφή του παράλληλου αλγορίθμου, στατιστικά αποτελέσματα μετρήσεων τόσο από τοπικούς υπολογιστές όσο και από τον Διάδη. Επίσης, αναφέρονται πιθανά μειονεκτήματα της υλοποίησης, ή και άλλες μέθοδοι που θα μπορούσαν να χρησιμοποιηθούν για την παραλληλοποίηση. Τέλος, ευχαριστούμε τον μεταπτυχιακό φοιτητή Σισμάνη Νικόλαο που ήταν πάντα διαθέσιμος να απαντήσει στις απορίες μας κατά την υλοποίηση της εργασίας. Καλή ανάγνωση... 2 Ο σειριακός k nearest neighbor algorithm Η κατανόηση της σειριακής έκδοσης του συγκεκριμένου αλγορίθμου είναι πολύ βασική πριν ξεκινήσουμε οιαδήποτε περαιτέρω ανάλυση που θα αφορά την παραλληλοποίησή του. Ουσιαστικά πρόκειται για μια τυποποιημένη διαδικασία μέσω της οποίας αρχικά ορίζουμε ένα αυθαίρετα μεγάλο, αλλά πεπερασμένο σύνολο, από n σημεία στο d διάστατο (d Z + ) επίπεδο. Επίσης, ορίζουμε ένα πεπερασμένο σύνολο από q σήμεια, στο ίδιο επίπεδο. Τέλος, θέλουμε να βρούμε τα k κοντινότερα σημεία για καθένα από αυτά τα q σημεία, δοθέντος αριθμού k (k N). Παρακάτω, αναλύεται τι γίνεται ακριβώς σε κάθε ένα από τα επιμέρους αρχεία του σειριακού που δόθηκε. 2.1 Η λειτουργία του knntest.c Οπως αναφέρθηκε ανωτέρω, χρειαζόμαστε n τυχαία σημεία. Η διαδικασία αυτή υλοποιείται μέσα στο συγκεκριμένο αρχείο. Το εκτελέσιμο πρόγραμμα απλώς θα δέχεται σαν ένα από τα ορίσματα τον αριθμό των σημείων n κι έπειτα η συνάρτηση παραγωγής τυχαίων αριθμών που βρίσκεται μέσα στο αρχείο θα δώσει τα εκάστοτε σημεία στον d-διάστατο χώρο. Εντελώς αντίστοιχα θα πάρουμε και τα q σημεία στον ίδιο χώρο. Δηλαδή, και πάλι θα δίνει ο χρήστης ένα όρισμα για τον αριθμό των σημείων q κι έπειτα αυτά τα σημεία θα παράγονται τυχαία. Οι τιμές τους θα κυμαίνονται (όσον αφορά τις διαστάσεις) στο [ 50, 50], τόσο 2
για τα n, όσο και για τα q σημεία. Μετά τον ορισμό λοιπόν των σημείων n, q, θα δίνει ο χρήστης επίσης τον αριθμό των d διαστάσεων κι επίσης τον αριθμό των γειτόνων k. Στην παράλληλη έκδοση, θα δίνεται κι ο αριθμός των threads. Τέλος, η knntest.c περιέχει χρήσεις συγκεκριμένων συναρτήσεων για τη μέτρηση χρόνου εκτέλεσης του προγράμματος αλλά και για την αποθήκευση των τελικών αποτελεσμάτων σε αρχεία bin για την μετέπειτα επεξεργασία από το matlab, καθώς και την συνάρτηση knns(), μέσω της οποίας μεταφέρονται όλα τα απαραίτητα στοιχεία στην knns.c για την υλοποίηση του αλγορίθμου. 2.2 Η λειτουργία του knns.c Μέσα στο συγκεκριμένο αρχείο, υπάρχει η βασική συνάρτηση knns(), που δέχεται από την knntest.c το σύνολο των σημείων q και n, τον αριθμό των γειτόνων k, στην παράλληλη έκδοση τον αριθμό των threads, και επιστρέφει τον πίνακα με τις αποστάσεις των κοντινότερων k γειτόνων, αλλά και τους δείκτες των στοιχείων αυτών. Για να βρεθούν αυτές οι αποστάσεις και οι δείκτες ακολουθούνται μια σειρά από κλήσεις συναρτήσεων που διεκπεραιώνουν συγκεκριμένες, διακριτές λειτουργίες. Αρχικά, μέσα στην knns() γίνεται δυναμική δέσμευση μνήμης για q n θέσεις. Ουσιαστικά, μέσα σε αυτόν τον πίνακα θα βρίσκονται όλες οι αποστάσεις (συνολικά n, μιας τόσα είναι τα σημεία) για κάθε σημείο του συνόλου q. Η οπτικοποίηση του παραπάνω φαίνεται στο ακόλουθο σχήμα: 1...n 1 2... q Στη συνέχεια, καλείται η comute_distances, η οποία δέχεται ως ορίσματα τα q και n σημεία και τον πίνακα distances. Αυτή, μέσω της συνάρτησης squared_distance(), υπολογίζει όλες τις αποστάσεις για καθένα σημείο q ως προς τα σημεία n και τις αποθηκεύει στον πίνακα results, ο οποίος είναι ουσιαστικά ο πίνακας distances. Στη συνέχεια, καλείται η selection(), η οποία μέσω των επιμέρους συναρτήσεων select_nearest_k() και max_index(), βρίσκει τους κοντινότερους k γείτονες για καθένα από τα q σημεία, με τις αποστάσεις τους από το εκάστοτε q σημείο και τον αύξοντα αριθμό τους (index). Να σημειωθεί πως ονόματα συναρτήσεων και μεταβλητών τα έχουμε τροποποιήσει, ώστε να είναι περισσότερο βολικά για μας κατά την υλοποίηση. Αναλυτικά, μπορείτε να δείτε τα αντίστοιχα αρχεία κώδικα. 3
3 Ιδέες παραλληλοποίησης Εχοντας αναλύσει τον σειριακό κώδικα, στο συγκεκριμένο μέρος θα προσπαθήσουμε να αναλύσουμε τις ιδέες παραλληλοποίησης αλλά και τους προβληματισμούς που είχαμε ως προς την απόδοσή τους. Ο τρόπος που επιλέξαμε τελικά, πολύ πιθανόν να τρέχει αποδοτικότερα σε συγκεκριμένα datasets και queries. Ενθαρρύνουμε τον αναγνώστη να δημιουργήσει όλες τις παραλληλοποιήσεις που αναφέρονται εδώ, ώστε να έχει μια πιο γενική εικόνα του προβλήματος. 3.1 Διαχωρίζοντας το dataset Η πρώτη ιδέα που σκεφτήκαμε, αλλά που τελικά απορρίψαμε, ήταν να διασπάσουμε το σύνολο των n σημείων (dataset) και συγκεκριμένο thread να υπολογίζει τις αποστάσεις για συγκεκριμένο αριθμό σημείων. Δηλαδή, δοθέντων threads, έστω, το κάθε thread θα εκτελούσε n αποστάσεις, ενώ το τελευταίο n + (n mod ). Ο λόγος που απορρίφθηκε η συγκεκριμένη ιδέα ήταν γιατί η μέθοδος που περιγράφεται τελευταία δίνει περισσότερα resources σε κάθε thread (περισσότερες συναρτήσεις δηλαδή), κι έτσι μεγαλύτερο μέρος του προγράμματος υλοποιείται παράλληλα. Αυτό έχει επίπτωση στον χρόνο, μιας και ένας υπολογιστής με δύο πυρήνες, όταν χρησιμοποιούνται και οι δύο, ιδεατά μειώνει στο μισό τον χρόνο εκτέλεσης ενός προγράμματος, από το να εκτελείται σειριακά το ίδιο πρόγραμμα μόνο σε έναν. 3.2 Διαχωρίζοντας τα queries Στα ίδια πλαίσια με την προηγούμενη ιδέα, ήταν να χωρίσουμε αντίστοιχα αυτήν την φορά τα σημεία q (το σύνολο των σημείων που απαρτίζουν τα queries δηλαδή). Παρακάτω, στο πρώτο υποκεφάλαιο αναλύεται η απλή λογική διαχωρισμού, η οποία ουσιαστικά θα προσέφερε ότι και η πρώτη διαδικασία παραλληλοποίησης των n σημείων, ενώ στο δεύτερο η λίγο πιο σύνθετη, αλλά τελικά αποδικότερη έκδοση. 3.2.1 Η απλή εκδοχή Η ιδέα είναι, δοθέντος του αριθμού των threads και του συνόλου των queries, έστω και q, αντίστοιχα, το κάθε thread να κάνει ό,τι και η σειριακή έκδοση του προγράμματος, αλλά για q queries. Δηλαδή, να έχουμε ένα block από queries σε κάθε thread, τα οποία τελικά εκτελούνται παράλληλα. Επισήμανση: Οπως και προηγουμένως, έτσι και τώρα, το τελευταίο thread θα πρέπει να τρέξει συνολικά q + (q mod ) queries, ώστε να καλυφθεί όλο το σύνολο 4
των σημείων, μιας και η διαίρεση q, σπάνια είναι ακέραια. Αφού εκτελεστεί λοιπόν το κάθε thread και τοποθετήσει στις αντίστοιχες θέσεις του πίνακα results (distances) τις αποστάσεις από τα n σημεία του dataset, τελειώνει τη διεργασία του, γίνεται join με τα υπόλοιπα και εν συνεχεία τερματίζουν. 3.2.2 Η πιο σύνθετη, αλλά αποδοτικότερη Η λίγο πιο σύνθετη, αλλά αποδοτικότερη λύση που και τελικά εφαρμόσαμε, ή- ταν να μην τερματίσουμε το thread με την ολοκλήρωση της τοποθέτησης των αποστάσεων από τα n σημεία στον πίνακα results, αλλά να πάμε ένα βήμα παραπέρα και να εφαρμόσουμε το εξής: αφού καταχωρηθούν στον πίνακα results οι αποστάσεις, να συνεχίσει το thread και να βρει τους k κοντινότερους γείτονες για κάθε ένα query του συγκεκριμένου block που είναι αρμόδιο. Με αυτόν τον τρόπο πετυχαίνουμε αυτό που αναφέραμε στην αρχή. Να παραλληλοποιήσουμε δηλαδή ακόμη μεγαλύτερο μέρος του προγράμματος και τελικά να έχουμε γρηγορότερο χρόνο εκτέλεσης. Μια άλλη ιδέα που είχαμε, αλλά που τελικά δεν την κρίναμε εξίσου αποδοτική, ήταν η εξής: αφού τερματίσουν τα threads, να δημιουργηθούν εκ νέου, ώστε το κάθε ένα από αυτά να αναλάβει να βρει τους κοντινότερους γείτονες για συγκεκριμένα blocks από queries. Επειδή το κάθε thread θέλει κάποιο χρόνο να δημιουργηθεί, να γίνει join, κ.λπ., αυτή η ιδέα επίσης απορρίφθηκε, καθώς θα απαιτούσε επιπλέον χρόνο για τη δημιουργία των νέων threads. 4 Ο αλγόριθμος που χρησιμοποιήσαμε Ο αλγόριθμος που τελικά χρησιμοποιήσαμε για την ανάπτυξη του προγράμματος ήταν αυτός που αναφέρθηκε στην υποενότητα 3.2.2. Για να έχουμε μια καλύτερη οπτική επαφή, παρακάτω παρουσιάζουμε τον τρόπο με τον κάθε thread είναι αρμόδιο για την εκτέλεση του δικού του block από queries: first block of queries -th block of queries 1... q 1 2... Παρατηρήστε ότι σκόπιμα το τελευταίο thread πιάνει μεγαλύτερη έκταση από τα υπόλοιπα, μιας και σε αυτό προστίθεται ένας επιπλέον αριθμός από queries, που είναι ίσος με το υπόλοιπο της ευκλείδειας διαίρεσης του q με το. 5
4.1 Ανάλυση του threading Αυτό που κάνουμε είναι αφού καλεστεί η συνάρτηση comute_distances() από την main(), μέσα σε αυτή να δημιουργούμε αρχικά μία ξεχωριστή δομή για κάθε thread. Αυτό το κάνουμε, διότι γίνεται να περάσουμε μόνο έναν ointer τύπου void μέσα στη συνάρτηση που θα καλεί κάθε φορά η thread_create() για τη δημιουργία του thread. Επομένως, στην περίπτωση που χρειαζόμαστε πολλές μεταβλητές, αυτές τις περνάμε σε μια δομή και στη συνέχεια καλούμε τη διεύθυνση της δομής μέσω της thread_create() και μεταβιβάζεται έτσι η δομή στη συνάρτηση που καλείται επίσης από την thread_create(). Η συνάρτηση που καλούμε την έχουμε ονομάσει block_distances(), αφού αφορά τις αποστάσεις που αποθηκεύονται στον πίνακα results για το κάθε ένα από τα n σημεία του dataset και του query που βρίσκονται μέσα στο τρέχον block από queries του συγκεκριμένου thread που εκτελείται. Το ερώτημα που τίθεται βέβαια είναι το εξής: Πώς θα δηλώσουμε σε ποιο block από queries βρισκόμαστε και πώς θα δηλώσουμε τον ακριβή δείκτη του πίνακα results, μέσα στον οποίο θα πάει να γράψει το κάθε thread; Η απάντηση στην πρώτη ερώτηση είναι απλή. Γνωρίζοντας τον αριθμό των queries που θα ανήκουν σε κάθε block, έστω block, κάθε φορά που δημιουργούμε μέσω μίας for έναν καινούριο thread μέσω της thread_create(), θα περνάμε ως όρισμα μέσω της δομής που δημιουργούμε κι έναν counter, ο οποίος θα έχει κάθε φορά την τιμή counter = i block. Ετσι προσανατολίζουμε το thread και του καθορίζουμε σε αρχική φάση, σε ποιο block βρισκόμαστε. Αρκεί όμως αυτό;. Οχι. j i-th query... 1 2 n first block of queries -th block of queries 1... q 1 2... Θα πρέπει κάθε φορά ο δείκτης του πίνακα results, πέρα από το να ξέρει σε ποιο block από queries βρισκόμαστε και να προσπεράσει τις αντίστοιχες counter n θέσεις, να ξέρει και σε ποιο τρέχον query του block βρισκόμαστε. Επομένως, μέσα στην block_distances(), έχουμε μια for που σαρώνει όλα τα queries του 6
block κι έτσι ο δείκτης μας αποκτά τη μορφή counter n+i, όπου i είναι το τρέχον query σε αύξουσα σειρά που σαρώνεται. Τέλος, θα πρέπει να δηλώνουμε μέσω μιας εσωτερικής for το ποια απόσταση ακριβώς υπολογίζεται. Δηλαδή, το συγκεκριμένο query με ποιο ακριβώς από τα n σημεία του dataset υπολογίζουμε τη μεταξύ τους απόσταση. Τελικά, ο δείκτης του πίνακα results, όπως αναφέρεται και με σχόλιο μέσα στον κώδικα θα πάρει τη μορφή counter n+i+j, όπου j είναι το τρέχον στοιχείο της εσωτερικής for που ανήκει στο dataset. Αντίστοιχη διαδικασία χρησιμοποιείται και για την επιλογή του query που θα στείλουμε στην squared_distance() για τον υπολογισμό των αποστάσεων. Στην προκειμένη περίπτωση απλώς χρειάζεται μόνο το (counter + i) d, για να ξέρουμε ποιου query τις διαστάσεις να επιλέξουμε. Η block_distances() ολοκλήρωνεται καλώντας block φορές την select_nearest_k(), έτσι ώστε το ίδιο thread, όπως αναφέραμε και πριν, να βρίσκει τους k κοντινότερους γείτονες για κάθε ένα από τα queries που του αντιστοιχούν (υπόψη ότι η select_nearest_k() υπολογίζει τους k κοντινότερους γείτονες μόνο για ένα query, γι αυτό και το καλούμε block φορές). Τέλος, με το που τελειώσει την εργασία της η block_distances(), επιστρέφουμε στην comute_distances() όπου και γίνονται join τα threads και τελειώνει και το πρόγραμμά μας. 4.2 Η απόδοσή του σε γράφημα Αφού τελειώσαμε με το θεωρητικό κομμάτι, αλλά και με την εξήγηση των βασικών μερών του προγράμματος, θα παρουσιάσουμε παρακάτω την απόδοση του παράλληλου αλγορίθμου μας σε σύγκριση με τον δοθέντα σειριακό. Να σημειωθεί πως οι τιμές του χρόνου των εκτελέσεων είναι οι μέσοι όροι, όπως αυτοί προέκυψαν από πολλαπλές εφαρμογές του ίδιου προγράμματος, με ίδια ορίσματα και ίδιο αριθμό threads κάθε φορά, έτσι ώστε να προσεγγίζουν το δυνατόν την πραγματικότητα. 4.2.1 Στον Διάδη Ο Διάδης είναι ο κεντρικός διακομιστής (server), διαθέτει τέσσερις πυρήνες και οκτώ actual threads. Οι πρώτες μας δοκιμές έγιναν απομακρυσμένα, στο συγκεκριμένο server. Λόγω των επεξεργαστικών δυνατοτήτων του, θεωρητικά είναι πιο robust στις εκτελέσεις μας. Το πρόβλημα που παρουσιαζόταν ήταν πως αρκετοί χρήστες ήταν συνδεμένοι στο δίκτυο την ίδια στιγμή με αποτέλεσμα να μη γνωρίζουμε κατά πόσο τα αποτελέσματα είναι όντως τα αναμενόμενα. Χρησιμοποιώντες την εντολή to του unix, προσπαθούσαμε να εκτελέσουμε το πρόγραμμα οποτεδήποτε οι χρήστες ζητούσαν τα λιγότερα resources. Το πρόγραμμα με συγκεκριμένα ορίσματα, δηλαδή συγκεκριμένο αριθμό n, d, q, k, την φορά, εκτελέστηκε περίπου δέκα φορές. Αυτό έγινε για περίπου δέκα 7
δεκαπέντε διάφορες περιπτώσεις testcases, από δέκα εκτελέσεις τη φορά, στα οποία έτρεχε ως αναμενόταν όλες τις φορές. Παραθέτουμε ενδεικτικά το γράφημα χαρακτηριστικά μεγάλων τιμών, ώστε να φανεί η δύναμη και η ισχύς του multithreading. Κάτι που ξεκινάει και φαίνεται να τρέχει αργά, τελικά καταλήγει να τερματίζει γρηγορότερα από ό,τι θα περίμενε κανείς! Οι τιμές που δώσαμε λοιπόν για inut ήταν οι εξής: n = 500000 d = 25 q = 1000 k = 50 Να αναφερθεί πως στο γράφημα που ακολουθεί δε συμπεριλήφθηκαν οι τιμές για threads > 8, μιας και για 16, 32,... threads οι επιδόσεις του Διάδη αρχικά παρέμειναν στα επίπεδα των 8 threads, αλλά στη συνέχεια άρχισαν να πέφτουν. Ουσιαστικά έτσι συνειδητοποιεί κανείς ότι το threading δεν είναι πανάκεια και είναι καλό να το χρησιμοποιούμε μόνο για όσα actual threads έχει ο επεξεργαστής μας, ή αν χρησιμοποιήσουμε παραπάνω, να μην χρησιμοποιήσουμε πολλά παραπάνω. Το γράφημα, ύστερα από την εύρεση των μέσων όρων για 1, 2, 4, 8 threads αντίστοιχα, ήταν το εξής: t(s) 44.6 27.4 17.1 9.6 1 2 4 8 threads 8
4.2.2 Σε οικιακό μηχάνημα Οι επιδόσεις του οικιακού μηχανήματος προφανώς δε μπορούν να συναγωνιστούν τις αντίστοιχες ενός server όπως του Διάδη, αλλά αποτελούν και αυτές μια ένδειξη του τι ακριβώς συμβαίνει στον υπολογιστή του καθενός από εμάς κι όχι απαραίτητα σε έναν server. Επίσης, αξίζει να αναφερθεί ένα πλεονέκτημα του οικιακού υπολογιστή σε σχέση με τον Διάδη, αλλά και ένα μειονέκτημα. Το πλεονέκτημα είναι ότι στον υπολογιστή έχουμε (συνήθως) πρόσβαση μόνο εμείς, άρα μόνο εμείς είμαστε αυτοί που ζητάμε πόρους από τον επεξεργαστή κι έτσι ξεχνάμε το πρόβλημα που συναντούσαμε στον Διάδη με τους πολλούς χρήστες. Ωστόσο, σε υπολογιστές που τρέχουν λειτουργικά συστήματα όπως τα windows, είναι αδύνατον να απομονώσεις την επεξεργαστική ισχύ του υπολογιστή και να διοχετεύσεις όλους τους πόρους στην εκτέλεση του προγράμματος. Επόμενως, θα πρέπει να ληφθεί ρητά υπόψη ότι την ώρα εκτέλεσης του συγκεκριμένου προγράμματος, στον υπολογιστή μας τρέχουν και δευτερεύουσες εφαρμογές που καταναλώνουν αρκετή ram αλλά και cu (antiviruses, skye, browsers, etc). Το λειτουργικό σύστημα που χρησιμοποιήσαμε για να δαπανήσουμε το κατά δυνατόν λιγότερο πόρους ήταν το debian, linux. Δεδομένων αυτών, ας περάσουμε στο κομμάτι των μετρήσεων. Εγιναν σε netbook με επεξεργαστή ATOM 1.5GHz dual core, με υποστήριξη τεσσάρων actual threads. Αυτή την φορά δώσαμε μικρότερες τιμές, ώστε να δώσουμε κι ένα γραφικό αποτέλεσμα του τι γίνεται όταν οι τιμές δεν είναι ακραίες και για να διαπιστωθεί ότι ο αλγόριθμος τρέχει ως αναμενόταν. Τα δεδομένα που δόθηκαν ήταν τα εξής (προφανώς δε μπορούσαμε να δώσουμε τα δεδομένα που δώσαμε στον Διάδη ώστε να δούμε όλα τα testcases που επιχειρήσαμε εκεί, καθώς δεν έχουμε τόσο μεγάλη ram): n = 100000 d = 25 q = 1000 k = 50 Αντίστοιχα κι εδώ, επειδή οι actual threads του επεξεργαστή είναι 4, στο γράφημα παρουσιάζονται οι επιδόσεις αυτών. Στην προσθήκη περισσότερων από 4 threads το πρόγραμμα στον χρόνο εκτέλεσης αρχικά διατηρούνταν στα ίδια πάνω κάτω επίπεδα για 8, 16 threads. Από 64 και πάνω threads άρχισε να αυξάνεται αισθητά σε σχέση με τον αρχικό χρόνο εκτέλεσης σε μια κλίμακα της τάξης του 5 10%. Αξίζει να σημειωθεί πως τα testcases για δύο διαστάσεις έδιναν σωστό γράφημα στο matlab, γεγονός που επαληθεύει τη σωστή λειτουργία του προγράμματός μας. 9
t(s) 29.6 15.1 8.0 1 2 4 threads 5 Επίλογος Φτάνοντας στο τέλος, ελπίζουμε να καλύψαμε τις βασικές πτυχές της παραλληλοποίησης και να έγινε κατανοητό μέσα από τα γραφόμενά μας τόσο το multithreading, όσο και η βαθύτερη κατανόηση του k-nearest neighbor algorithm. Το hyhenation των ελληνικών λέξεων από το L A TEX 2ε γίνεται αυτόματα, ο- πότε ζητούμε συγγνώμη για τυχόν λάθος. Τα σχήματα έγιναν στο GIMP. Η υλοποίηση του κώδικα έγινε ύστερα από στενή συνεργασία των δύο φοιτητών. Η επιμέλεια τόσο του παρόντος df αναφοράς, όσο και των σχημάτων, έγινε από τον φοιτητή Μόσχογλου Στυλιανό. Ευχαριστούμε πολύ για την ανάγνωση... Μόσχογλου Στυλιανός Μικρόπουλος Αριστοτέλης 10