GCC: Tutorial για να μεταγλωττίζετε κώδικα με βελτιστοποιήσεις

dimitris | Παρ, 08/30/2013 - 07:28 | 32'

Οι διανομές Linux διαθέτουν αποθετήρια με χιλιάδες έτοιμες εφαρμογές για άμεση εγκατάσταση. Αν όμως θέλετε να εγκαταστήσετε ένα πρόγραμμα και δεν υπάρχει έτοιμο πακέτο για εγκατάσταση, τότε χρειάζεστε έναν "compiler" και γνώσεις για το πως να τον βελτιστοποιήσετε. Σε αυτό το άρθρο βλέπουμε πως να μεταγλωττίζουμε κώδικα με τις βελτιστοποιήσεις του GCC.

Ας υποθέσουμε το εξής σενάριο. Κατεβάσατε από την ιστοσελίδα του Χ καταπληκτικού προγράμματος ένα tarball, το αποσυμπιέσατε και τελικά φτάσατε σε κάποιο README αρχείο που λέει ότι πρέπει να το κάνετε "compile", δηλαδή να μεταγλωττίσετε το πρόγραμμα από τον κώδικά του για να παραχθεί ένα εκτελέσιμο αρχείο. Αν δεν το κάνετε αυτό, απλά το πρόγραμμα δεν τρέχει.

Γι' αυτό, θα χρειαστείτε ένα μεταγλωττιστή (compiler) στο Linux σας. Και ο πιο γνωστός και καλύτερος compiler στο Linux είναι βασικά η... συλλογή μεταγλωττιστών GNU (GNU Compiler Collection), πιο γνωστή ως GCC.

Οι τρεις (συνήθεις) εντολές για compile

Στις περισσότερες περιπτώσεις μπορείτε απλώς να χρησιμοποιήσετε την αλυσίδα εντολών: ./configure && make && su -c 'make install'. Αυτή η αλυσίδα εκτελεί ένα σύνολο κανόνων για να δημιουργήσει την εφαρμογή σας που περιγράφονται σε ένα ή περισσότερα αρχεία που λέγονται Makefiles. Συνήθως, αυτά τα Makefiles περιέχουν μια γραμμή με παραμέτρους που μοιάζει με αυτή:

CFLAGS=”-O2 -Wall -march=pentium4 -ffast-math -I.. -L...”

Αυτές οι παράμετροι (εξίσου γνωστές ως επιλογές ή διακόπτες ή flags) μοιάζουν πολύπλοκες και δύσκολα θα ασχοληθεί κανείς μαζί τους, ωστόσο μπορεί να θελήσετε να τις τροποποιήσετε για να φτιάξετε ένα εκτελέσιμο που θα είναι προσαρμοσμένο στο σύστημά σας ή να λύσετε προβλήματα μεταγλώττισης.

Σε αυτό το tutorial θα δούμε τις πιο συνηθισμένες από τις παραμέτρους του μεταγλωττιστή GCC, που θα κάνουν τη ζωή σας εύκολη όταν έχετε να αντιμετωπίσετε δύστροπο κώδικα που... αρνείται να υποστεί το make. Επίσης, θα σας δώσουμε και μερικές συμβουλές για το πως να τεστάρετε τις εφαρμογές με το GCC και κάποια σχετικά εργαλεία.

Τι σημαίνει "μεταγλώττιση"

Για να καταλάβει κανείς τη λογική της μεταγλώττισης από τον κώδικα πρέπει να κάνει ένα βήμα πίσω και να κοιτάξει γενικότερα τη λογική της συγγραφής κώδικα. Τα προγράμματα συνήθως γράφονται σε κάποια γλώσσα προγραμματισμού (C, C++, PHP, κ.α.) η οποία καθορίζεται από ένα απλό σύνολο κανόνων που οι άνθρωποι (οι προγραμματιστές) μπορούν να κατανοήσουν, αλλά οι υπολογιστές δεν μπορούν.

Οι προγραμματιστές χρησιμοποιούν την σύνταξη της γλώσσας για να γράψουν πηγαίο κώδικα, δηλαδή ένα ή περισσότερα αρχεία που μπορούν να διαβαστούν και να κατανοηθούν από οποιονδήποτε προγραμματιστή που γνωρίζει την ίδια γλώσσα. Οι υπολογιστές, από την άλλη, δεν καταλαβαίνουν τις γλώσσες προγραμματισμού. Το μόνο που ξέρουν είναι τα σημεία "μηδέν" και "ένα", δομημένα με ένα τρόπο συγκεκριμένο για κάθε αρχιτεκτονική και για κάθε λειτουργικό σύστημα που τρέχουν.

GCC: Ο παγκόσμιος μεταγλωττιστής

Ένας μεταγλωττιστής (compiler) δρα σαν μεταφραστής ανάμεσα στον άνθρωπο και την μηχανή, γι' αυτό και λέγεται μετα-γλωττιστής. Οι μεταγλωττιστές μπορούν συνήθως να υποστηρίζουν αρκετές γλώσσες προγραμματισμού (με τα front-ends) και διαφορετικές αρχιτεκτονικές (με τα λεγόμενα back-ends). Για παράδειγμα, ο GCC έχει front-ends για C, C++, Java, Objective C, ADA και Fortran, ενώ έχει back-ends για αναρίθμητες αρχιτεκτονικές και σχεδόν όλα τα γνωστά λειτουργικά συστήματα: Linux, Mac OS X, BSD, Unix παράγωγα αλλά και τις σύγχρονες εκδόσεις των Windows.

Σε αυτό το άρθρο χρησιμοποιούμε το GCC για Linux σε αρχιτεκτονική Intel, αλλά τα περισσότερα που θα διαβάσετε ισχύουν και για κάθε άλλο συνδυασμό πλατφόρμας και λειτουργικού συστήματος.

Ο απλούστερος τρόπος να μεταγλωττίσετε ένα πρόγραμμα είναι να χρησιμοποιήσετε το GCC από τη γραμμή εντολών. Για παράδειγμα, γράφοντας

gcc program.c

θα παραχθεί ένα αρχείο με το στάνταρ όνομα a.out, το οποίο είναι η μετάφραση του program.c σε μορφή εκτελέσιμη από τον υπολογιστή. Στην παραπάνω εντολή, λοιπόν, το program.c είναι ο πηγαίος κώδικας (που έχουμε στο τρέχον υποκατάλογο) και gcc είναι το front-end του μεταγλωττιστή για την γλώσσα C, στην οποία θα περιοριστούμε προς το παρόν. Αυτό σημαίνει ότι δεν θα ασχοληθούμε με παραμέτρους οι οποίες αφορούν front-ends για αλλες γλώσσες. Αν θέλετε να έχετε το εκτελέσιμο με άλλο όνομα, τότε χρησιμοποιήστε την παράμετρο -ο, ως εξής:

gcc program.c -o myexe

Θα δημιουργηθεί ένα εκτελέσιμο με το όνομα myexe. Η επιλογή αυτή είναι μία από τις πολλές που αλλάζουν την συμπεριφορά του GCC. Μια πιο πλήρης εντολή μεταγλώττισης που μιμείται ότι γίνεται σε ένα Makefile είναι η παρακάτω:

gcc $CFLAGS program.c -o myexe

όπου CFLAGS είναι μια μεταβλητή που μπορεί να τεθεί προσωρινά για την κονσόλα που βρίσκεστε ή μόνιμα για κάθε μελλοντική χρήση αν χρησιμοποιείτε συχνά τις ίδιες επιλογές (τα flags) του μεταγλωττιστή. Για παράδειγμα, αν έχετε δώσει προηγουμένως κάτι σαν κι αυτό:

export CFLAGS=”-O3 -Wall -march=pentium4 -ffast-math -I.. -L..”

τότε η προηγούμενη εντολή μεταγλώττισης θα επεκταθεί αυτόματα σε:

gcc -O3 -Wall -march=pentium4 -ffast-math -I.. -L.. program.c -o myexe

Το βασικό ερώτημα που θα σας βασανίζει τώρα είναι προφανώς, “Ποιες πρέπει να είναι οι παράμετροι στην CFLAGS και, κυρίως, γιατί;”Η απάντηση εξαρτάται από το κώδικα και την χρήση του παραγόμενου εκτελέσιμου. Παρακάτω θα συζητήσουμε μερικές συνήθεις επιλογές.

Σχετικά με την βελτιστοποίηση

Μέχρι τώρα αναφερθήκαμε στον μεταγλωττιστή σαν εργαλείο μετάφρασης των εντολών μιας γλώσσας προγραμματισμού σε δυαδικό κώδικα που θα είναι εκτελέσιμος από τη μηχανή. Όμως, ένας μεταγλωττιστής είναι συχνά κάτι περισσότερο από αυτό: στην διαδικασία παραγωγής του εκτελέσιμου, ένας καλός μεταγλωττιστής μπορεί να αναδομήσει τον πηγαίο κώδικα έτσι ώστε να γίνει πιο αποτελεσματικός χωρίς να μεταβληθεί το αναμενόμενο αποτέλεσμα.

Αυτοί οι μετασχηματισμοί λέγονται βελτιστοποιήσεις (optimizations) και επιταχύνουν την εκτέλεση του τελικού προγράμματος. Ένα σχετικό παράδειγμα είναι η εξάλειψη νεκρού κώδικα (dead code). Υποθέστε ότι έχετε μια περίπλοκη ρουτίνα για τον υπολογισμό της τιμής μιας μεταβλητής που δεν χρησιμοποιείται ποτέ. Η εξάλειψη των κλήσεων σε αυτήν την ρουτίνα συνεπάγεται μεγάλο κέρδος στο χρόνο εκτέλεσης.

Βέβαια, δεν είναι όλα τόσο εύκολα. Η βελτιστοποίηση αυξάνει τον χρόνο μεταγλώττισης και το μέγεθος του τελικού εκτελέσιμου. Οι μετασχηματισμοί βελτιστοποίησης που κάνει ο GCC είναι ομαδοποιημένοι σε τρεις global διακόπτες ανάλογα με το κόστος τους, δηλαδή την επιβράδυνση που θα επιφέρουν στην διαδικασία μεταγλώττισης ή την αύξηση του μεγέθους του εκτελέσιμου:

  • Ο διακόπτης -O1 ενεργοποιεί εκείνες τις βελτιστοποιήσεις που δεν έχουν αξιοσημείωτο αντίκτυπο στο χρόνο και το μέγεθος της μεταγλώττισης.
  • Ο -Ο2 κάνει όλους τους μετασχηματισμούς του -O1 και μερικούς ακόμα που προκαλούν αύξηση στο χρόνο μεταγλώττισης, αλλά όχι και εκείνους που οδηγούν σε αύξηση μεγέθους του εκτελέσιμου.
  • Τέλος, ο -Ο3 “ανοίγει” όλες τις ασφαλείς βελτιστοποιήσεις, αδιαφορώντας αν προκαλούν αύξηση χρόνου μεταγλώττισης ή μεγέθους αρχείου.

Λεπτομέρειες για τις συγκεκριμένες βελτιστοποιήσεις που ενεργοποιεί κάθε διακόπτης θα βρείτε στην man σελίδα του GCC. Είναι εφικτό να επιλέξετε μεμονωμένους μετασχηματισμούς βελτιστοποίησης, χρησιμοποιώντας τις αντίστοιχες flags.

Υπάρχουν μετασχηματισμοί που στοχεύουν αποκλειστικά σε μαθηματικές λειτουργίες και είναι πολύ χρήσιμοι όταν ο κώδικας σας περιέχει περίπλοκους υπολογισμούς. Όλοι αυτοί ενεργοποιούνται μέσω της παραμέτρου -ffast-math. Κάποιοι, όμως, αλλάζουν την διαδικασία στρογγυλοποίησης (rounding) των υπολογισμών, γι' αυτό πρέπει να τους χρησιμοποιείτε μόνο όταν γνωρίζετε τη λειτουργία τους.

Οι παραπάνω βελτιστοποιήσεις βασίζονται σε γενικότερες αρχές και είναι ανεξάρτητες της κάθε αρχιτεκτονικής. Βέβαια, υπάρχουν μετασχηματισμοί που αφορούν το είδος του επεξεργαστή (ιδιαίτερα, τον αριθμό και τα ονόματα των καταχωρητών καθώς και τη μορφή των δυαδικών αρχείων). Πολλοί από αυτούς ενεργοποιούνται από την επιλογή -mtune (που ρυθμίζει τα πάντα εκτός από το binary interface της εφαρμογής για το συγκεκριμένο επεξεργαστή) και την -mcpu (που παράγει κώδικα αποκλειστικά για συγκεκριμένο επεξεργαστή).

Για παράδειγμα, αν ξέρετε εκ των προτέρων ότι η εφαρμογή σας θα τρέξει μόνο σε Core 2 επεξεργαστή, μπορείτε να χρησιμοποιήσετε την παράμετρο -mcpu=core2, η οποία παράγει εκτελέσιμο κώδικα που είναι ασύμβατος με παλιότερους επεξεργαστές της Intel (Π.Χ. Pentium IV και τους i386 γενικότερα). Επίσης, η αρχιτεκτονική Core2 παρέχει επιτάχυνση των μαθηματικών λειτουργιών, πράγμα που μπορείτε να εκμεταλλευθείτε με την παράμετρο -mfpmath-sse η οποία θα χρησιμοποιήσει primitives για το σύνολο εντολών SSE.

Για να ελέγξετε την ποιότητα του παραγόμενου κώδικα μπορείτε να δημιουργήσετε τον αντίστοιχο assembly κώδικα. Αυτό γίνεται με την επιλογή -$. Έτσι η εντολή gcc -$ myprog.c παράγει το αρχείο myprog.s σε assembly. Ένα εκπαιδευμένο μάτι, λόγου χάρη, μπορεί να διαβάσει το myprog.s για να διαπιστώσει σε ποιο βαθμό χρησιμοποιούνται οι τυπικοί καταχωρητές της συγκεκριμένης αρχιτεκτονικής και πιθανώς να τροποποιήσει το κώδικα (πράγμα που δεν είναι τόσο εύκολο όσο η ανάγνωση του κώδικα της C) για καλύτερη επίδοση. Η σωστή χρήση των διαθέσιμων καταχωρητών έχει πάντως τεράστιο αντίκτυπο στην ταχύτητα.

Headers και βιβλιοθήκες

Η μεταγλώττιση ενός project με αρκετά αρχεία κώδικα είναι μια διαδικασία δύο βημάτων. Πρώτα, όλα τα αρχεία μετατρέπονται σε κώδικα εκτελέσιμο από τη μηχανή (object αρχεία). Σε αυτό το στάδιο το σύστημα δεν κάνει resolve τις κλήσεις σε ρουτίνες. Με άλλα λόγια δεν ελέγχει αν οι απαιτούμενες ρουτίνες είναι πράγματι διαθέσιμες, αλλά προχωρά θεωρώντας αυτές ως δεδομένες. Το δεύτερο στάδιο είναι το αποκαλούμενο linking (σύνδεση), οπότε δημιουργείται το τελικό εκτελέσιμο. Τότε, ο μεταγλωττιστής θα τυπώσει σφάλμα αν δεν μπορεί να βρει μια ζητούμενη βιβλιοθήκη ή μια ρουτίνα. Η διαδικασία make δεν είναι τίποτε άλλο παρά μια έξυπνη αυτοματοποίηση αυτών των δύο βημάτων.

Υποθέστε ότι έχουμε δύο αρχεία, τα mainprogram.c και το procedures.c, και ότι χρειαζόμαστε επίσης μια βιβλιοθήκη που λέγεται libsample και η οποία βρίσκεται στον υποκατάλογο /opt/sample/lib και της οποίας τα header αρχεία (τα οποία επίσης χρειαζόμαστε μια και γίνονται include από το procedures.c) περιέχονται στο υποκατάλογο /opt/sample/include. Για να μεταγλωττιστεί το πρόγραμμα θα χρειαστεί να δημιουργήσουμε πρώτα τα object αρχεία ως εξής:

gcc $CFLAGS -c mainprogram.c

που θα παράγει το αρχείο mainprogram.o και μετά να δώσουμε:

gcc -Ι/opt/sample/inlude $CFLAGS -c procedures.c

που θα παράγει το procedures.o. Στις παραπάνω γραμμές, η $CFLAGS καθορίζει διάφορους διακόπτες χωρίς να περιλαμβάνει μονοπάτια αναζήτησης ή βιβλιοθήκες. Η παράμετρος -Ι ακολουθούμενη από την τοποθεσία των αρχείων header δείχνει στο GCC που να ψάξει γι' αυτά τα header. Πολλαπλές τοποθεσίες αρχείων header καθορίζονται με πολλαπλές επαναλήψεις της -Ι. Για τη σύνδεση (linking), χρειαζόμαστε επίσης την βιβλιοθήκη, οπότε καλούμε το GCC ως εξής:

gcc -L/opt/sample/lib mainprogram.o procedures.o -lsample -o mainprogram

Εδώ, η -L καθορίζει που βρίσκονται οι βιβλιοθήκες και η -lsample λέει στο GCC να κάνει τη σύνδεση με την βιβλιοθήκη libsample. Και εδώ, είναι δυνατό να καθοριστούν αρκετές τοποθεσίες για βιβλιοθήκες και να συνδεθούν στο πρόγραμμα διάφορες βιβλιοθήκες επαναλαμβάνοντας την -L και την -l, αντίστοιχα, για όσους υποκαταλόγους και βιβλιοθήκες είναι απαραίτητο. Σημειώστε ότι για τη σύνδεση με την libsample δεν συμπεριλάβαμε το πρόθεμα 'lib' μετά το -l. Αυτό είναι γενικότερο φαινόμενο: οι βιβλιοθήκες είναι αρχεία με πρόθεμα 'lib' στο όνομα τους, το οποίο παραλείπουμε όταν κάνουμε τη σύνδεση. Παρατηρήστε, ακόμη, ότι στην εντολή σύνδεσης δεν ορίσαμε $CFLAGS, μια και τα object αρχεία είχαν ήδη δημιουργηθεί με τις απαιτούμενες βελτιστοποιήσεις, τις παραμέτρους γλώσσας και συγκεκριμένου επεξεργαστή.

Οι τρεις προηγούμενες εντολές είναι ισοδύναμες με την εξής εντολή:

gcc $CFLAGS -l/opt/sample/include -L/opt/sample/lib mainprogram.c procedures.c -lsample -o mainprogram

Στατικές και διαμοιραζόμενες βιβλιοθήκες

Οι βιβλιοθήκες, τώρα, χωρίζονται σε δύο είδη: τις διαμοιραζόμενες (shared), που διακρίνονται από την κατάληξη .so και οι στατικές που χαρακτηρίζονται από την κατάληξη .a. Οι πρώτες μπορούν να φορτωθούν όταν το ζητήσει κάποιο πρόγραμμα, ενώ οι τελευταίες είναι μέρος του ίδιου του τελικού εκτελέσιμου (και φορτώνονται απευθείας).

Επομένως, ένα εκτελέσιμο που χρησιμοποιεί διαμοιραζόμενες βιβλιοθήκες είναι μικρότερο σε μέγεθος, αλλά εξαρτάται πάρα πολύ από το host σύστημα στο οποίο θα τρέξει, και για να γίνουμε ακριβείς από τις βιβλιοθήκες του. Ένα στατικό εκτελέσιμο είναι μεγαλύτερο σε μέγεθος, αλλά μια και είναι αυτοτελές μπορεί να τρέξει σε οποιοδήποτε σύστημα που έχει συμβατή αρχιτεκτονική με το σύστημα στο οποίο δημιουργήθηκε.

Debugging

Όμως, τα πράγματα δεν πάνε συνήθως τόσο ομαλά όσο θα περίμενε κανείς. Όταν ένα πρόγραμμα δεν συμπεριφέρεται σωστά, πρέπει να βρείτε και να κατανοήσετε τη ρίζα του προβλήματος. Η αποσφαλμάτωση (debugging) είναι ένα σύνολο τεχνικών που επιτρέπουν να εστιάσετε την προσοχή σας σε μικρά κομμάτια κώδικα ενώ το πρόγραμμα εκτελείται με στόχο τον εντοπισμό του προβλήματος με τον πιο εύκολο τρόπο και με τη λιγότερη προσπάθεια όσον αφορά την τροποποίηση του κώδικα και τον χρόνο που απαιτείται γι' αυτό. Για να μεταγλωττίσετε ένα πρόγραμμα για αποσφαλμάτωση, το GCC πρέπει να κληθεί χωρίς βελτιστοποιήσεις και με την επιλογή -g, όπως στο επόμενο παράδειγμα:

gcc -g myprogram.c - o myprogram

Ένα μεταγλωττισμένο πρόγραμμα “ξεχνάει” τα αρχικά ονόματα των μεταβλητών που έδωσε ο προγραμματιστής. Η παράμετρος -g σας επιτρέπει να ανιχνεύσετε τα αρχικά ονόματα των μεταβλητών στη C, C++ ή όποια άλλη γλώσσα χρησιμοποιήθηκε στον κώδικα. Όταν το εκτελέσιμο “τρέχει” με debugger, ο προγραμματιστής μπορεί να αλληλεπιδρά με το πρόγραμμα χρησιμοποιώντας τις αρχικές μεταβλητές.

Δημιουργώντας προφίλ

Σε πολλές εφαρμογές που είναι απαιτητικές σε πόρους επεξεργαστή, όπως η κωδικοποίηση βίντεο, ακόμη και οι πολύ προσεκτικά επιλεγμένες αυτόματες βελτιστοποιήσεις δεν είναι αρκετές για να επιτύχετε την βέλτιστη απόδοση. Σε αυτές τις περιπτώσεις, οι ζωτικότερες περιοχές του κώδικα απαιτούν κάποια "χεράτη" παρέμβαση. Τέτοιες βελτιστοποιήσεις όμως είναι δύσκολες στην υλοποίηση, και είναι αποτελεσματικές μόνο αν γίνουν στο κατάλληλο σημείο. Αυτό οφείλεται στο συχνό φαινόμενο το 90% του χρόνου εκτέλεσης ενός προγράμματος να καταναλώνεται από το 10% του κώδικα. Σε αυτό το 10% πρέπει να επικεντρωθεί ο προγραμματιστής, αν και δεν είναι πάντοτε προφανές ποια σημεία του κώδικα είναι τα πιο απαιτητικά σε υπολογιστική ισχύ.

Υπάρχει, όμως, μια εφαρμογή που μπορεί να βοηθήσει στην ανίχνευση αυτών των σημείων, το Gprof, που είναι μέρος του πακέτου binutils, και υπολογίζει το ποσοστό του χρόνου που χρησιμοποιείται μια συγκεκριμένη υπορουτίνα σε ένα κώδικα. Δείτε πως λειτουργεί με το παρακάτω παράδειγμα. Η proc1 καλείται 100.000 φορές, ενώ η proc2 10.000 φορές.

#include <stdio.h>
int i,j;
void  proc1 (int i);
void  proc2 (void);
int main () {
	for (j=0; j<10000; j++) 
		for (i=0; i<10; i++) 
			proc1(i);
	return 0;
}
void proc1 (int i) {
	if (i==4)	
		proc2();
}
void proc2 () {
	printf(“i is 4\n”);
}

Για να δείτε και να καταλάβετε την έξοδο του gprof στο παραπάνω κώδικα, μεταγλωττίστε το με την παράμετρο -pg:

gcc gproftest.c -pg -o gproftest 

όπου gproftest.c είναι το αρχείο με τον παραπάνω κώδικα

Μετά, εκτελέστε το πρόγραμμα με ./gproftest. Θα δημιουργηθεί το αρχείο gmon.out στο οποίο θα βασίσει την αναφορά του το gprof. Η ίδια η αναφορά παράγεται δίνοντας gprof gproftest, και θα τυπωθεί στην οθόνη. Αντίθετα, δίνοντας

gprof gproftest > prof.report

η έξοδος του gprof θα γραφτεί στο αρχείο gprof_report.

Η ίδια η αναφορά αποτελείται από δύο τμήματα, το flat profile και το διάγραμμα κλήσεων. Το flat προφίλ έχει πληροφορίες σχετικά με:

  • Το ποσοστό του χρόνου που χρειάζεται κάθε υπορουτίνα.
  • Το συνολικό χρόνο (δηλαδή το χρόνο που δαπανάται σε μια ρουτίνα και σε όλες που είναι πάνω από αυτήν στο δέντρο των κλήσεων).
  • Το χρόνο που δαπανάται σε κάθε ρουτίνα.
  • Τον αριθμό των κλήσεων σε κάθε μεμονωμένη ρουτίνα.
  • Το χρόνο σε νανοδευτερόλεπτα (10^-9 του δευτερολέπτου) ανά κλήση που απαιτείται για να ολοκληρωθεί κάθε μια ρουτίνα.
  • Το χρόνο σε νανοδευτερόλεπτα ανά κλήση που απαιτείται για να ολοκληρωθεί κάθε μια ρουτίνα και όλα τα παραγόμενα αυτής.
  • Το όνομα κάθε ρουτίνας.

Σε αυτό το απλό παράδειγμα η χρονομέτρηση είναι προσεγγιστική, μια και ο χρόνος εκτέλεσης είναι μικρός. Μπορείτε να πάρετε πιο σημαντικά αποτελέσματα όταν η εκτέλεση διαρκεί μερικά λεπτά οπότε το profiling αρχίζει να αποκτά νόημα.

Όπως υποδηλώνει το όνομα, το διάγραμμα κλήσεων είναι μια αναπαράσταση της ροής του προγράμματος διαμέσου των ρουτίνων (και λέγεται δέντρο). Το Gprof είναι ένα προηγμένο αλλά απλό στη χρήση εργαλείο, και καλό είναι να διαβάσετε τη man σελίδα του για οδηγίες εξειδικευμένης χρήσης.

Κάλυψη κώδικα

Τα μεγάλα project χρειάζονται μια κατανοητή σουίτα ελέγχου. Εδώ, το επίθετο 'κατανοητή' σημαίνει ότι με τους ελέγχους που παρέχονται, ο προγραμματιστής θα μπορεί να διαπιστώνει αν κάθε γραμμή κώδικα λειτουργεί όπως προβλέπεται σε κάθε δυνατή περίπτωση. Αυτό συνεπάγεται ότι η σουίτα ελέγχου πρέπει να φτάνει σε κάθε γραμμή κώδικα που μπορεί να εκτελεστεί από τον αλγόριθμο, δηλαδή όλες τις γραμμές του αν είναι καλογραμμένος. Είναι εύκολο να γράψει κανείς μια κατανοητή σουίτα ελέγχου αν το project είναι μικρό, όχι όμως και στα μεγάλα projects λόγω του πάρα πολλών διαφορετικών παραμέτρων που διαθέτουν αυτά.

Μπορείτε, ωστόσο, να χρησιμοποιήσετε το gcov, ένα εργαλείο που είναι μέρος του ίδιου του GCC. Συνδυαζόμενο με το gprof, το gcov είναι ένα τέλειο εργαλείο για τον έλεγχο του κώδικά σας· μπορεί να παράγει μέχρι και μια λεπτομερή αναφορά απαριθμώντας πόσες φορές χρησιμοποιήθηκε κάθε γραμμή κώδικα κατά την εκτέλεση του προγράμματος.

Για να πάρετε τέτοιου είδους πληροφορίες, πρέπει να μεταγλωττίσετε τον κώδικα με τις παραμέτρους -fprofile-arcs και -ftest-coverage. Πάρτε σαν παράδειγμα τον κώδικα του αρχείου gcovtest.c που είναι ο εξής:

#include <stdio.h>
int exec;
int i;
int main () 
{
	for (i=0; <3; i++)  {
		printf("This is a line\n");
	}
	if (exec){
		printf("This is another line\n");
	}
	return 0;
}

Για να ελέγξετε πόσες φορές εκτελείται κάθε γραμμή του παραπάνω κώδικα, πρέπει να τον μεταγλωττίσετε ως εξής:

gcc -fprofile-arcs -ftest-coverage gcovtest.c -o gcovtest

Ο μεταγλωττιστής θα παράγει το προαιρετικό αρχείο gcovtest.gcno, και όταν εκτελέσετε το πρόγραμμα θα δημιουργηθεί ένα ακόμα προαιρετικό αρχείο, το gcovtest.gcda. Τρέξτε, τώρα, το gcov, με όρισμα το πηγαίο αρχείο:

gcov gcovtest.c

Αυτό θα σας δώσει την παρακάτω έξοδο στο τερματικό:

File 'gcovtest.c'
Lines executed: 83.33% of 6
gcov.c:creating 'gcovtest.c.gcov'

H λεπτομερής ανάλυση θα γραφτεί στο αρχείο gcovtest.c.gcov.

Η παρακάτω είναι μια τυπική γραμμή αυτού του αρχείου:

3: 9: printf(“This is a line\n”);

όπου το 3 δηλώνει πόσες φορές εκτελέστηκε η συγκεκριμένη γραμμή κώδικα και το 9 είναι ο αριθμός της γραμμής αυτής μέσα στο πηγαίο αρχείο. Αμέσως μετά τυπώνεται ο ίδιος ο κώδικας της γραμμής. Στις γραμμές που παραλείπονται, μια παύλα αντικαθιστά το πλήθος των εκτελέσεων, ενώ οι γραμμές στις οποίες δεν φτάνει ποτέ ο αλγόριθμος έχουν πέντε διέσεις # στην αρχή. Μόνο οι γραμμές που περιέχουν εντολές λαμβάνονται υπόψη, ενώ ορισμοί και κενές γραμμές παραλείπονται. Περισσότερες πληροφορίες μπορείτε να διαβάσετε στις σελίδες man ή info του gcov.

Ο GCC είναι ένας ώριμος και πλήρης χαρακτηριστικών μεταγλωττιστής. Το εγχειρίδιο του (man) είναι μεγάλο και μερικές φορές τρομακτικό, αλλά τα καλά αποτελέσματα επιτυγχάνονται μόνο μέσω της κατανόησης του νοήματος των παραμέτρων του μεταγλωττιστή. Η λεπτομερής ρύθμιση αυτών για βέλτιστα αποτελέσματα είναι μια τέχνη και απαιτεί πολύ εμπειρία, ξεκάθαρη αντίληψη των στόχων και βαθιά γνώση του ίδιου του μεταγλωττιστή.

Συμφιλιωθείτε με τις δημοφιλέστερες παραμέτρους που παρουσιάστηκαν σε αυτό το tutorial και θα αποκτήσετε μια στέρεα βάση για να διερευνήσετε περισσότερο τις δυνατότητες του GCC.

Βάλτε τον compiler να βρίσκει τα προβλήματα

Ο GCC μπορεί να κάνει μερικά διαγνωστικά τεστ στον κώδικα και να προειδοποιήσει τον χρήστη για προβληματικές δομές. Η επιλογή -Wall ενεργοποιεί το μέγιστο επίπεδο προειδοποιήσεων, και είναι πολύτιμο εργαλείο για εντοπισμό πιθανών προβλημάτων σε πρώιμο στάδιο ανάπτυξης.

Cross-compiling

Ο GCC μπορεί να χρησιμοποιηθεί και σαν δια-μεταγλωττιστής (cross-compiler). Αυτό, στην πράξη, σημαίνει ότι τρέχοντας σε μια συγκεκριμένη αρχιτεκτονική μπορεί να παράγει κώδικα συμβατό για κάποια άλλη ασύμβατη αρχιτεκτονική. Ένας cross-compiler είναι ιδιαίτερα χρήσιμος όταν ο στόχος είναι ένα μηχάνημα μικρών δυνατοτήτων, όπως μια ενσωματωμένη (embedded) συσκευή, που συνήθως έχει μικρό χώρο αποθήκευσης και δεν είναι αρκετά γρήγορο για να τρέξει έναν μεταγλωττιστή. Για να κάνετε cross-compile, πρέπει να μεταγλωττίσετε τα πακέτα binutil, glibc και τον ίδιο το GCC από την αρχή με την παράμετρο –target=target-architecture στο script configure (αλλιώς το target θα τεθεί ίδιο με το σύστημα σας) και επίσης να αντιγράψετε τα σωστά include αρχεία στις κατάλληλες τοποθεσίες. Μια λεπτομερής ανάλυση της διαδικασίας αυτής είναι έξω από τους στόχους μας, αλλά αξίζει να σημειώσετε ό,τι όταν ρυθμιστεί ο cross-compiler θα δημιουργεί εκτελέσιμα με όλες τις δυνατές παραμέτρους για την αρχιτεκτονική που επιθυμείτε.

 

Δώσε αστέρια!

MO: 5 (ψήφοι: 1)