Απορία για τα #define

nilos | Πέμ, 06/23/2011 - 12:33 | 1' | 11

Γειά σας παιδιά,

έχω μια κουβέντα μ ένα φίλο μου και θέλω τη γνώμη σας. Συγκεκριμένα,
βλέπουμε οτι ο πηγαίος κώδικας του Linux έχει άπειρες συναρτήσεις οι
οποίες είναι υλοποιημένες με #define, για ποιό λόγο γίνεται αυτό; Είναι
επειδή μια συνάρτηση γραμμένη με #define είναι πιό γρήγορη απ την ίδια
αλλά γραμμένη "κανονικά" ή επειδη έτσι γράφτηκαν κι έτσι έμειναν;

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

MO: (ψήφοι: 0)

Σχόλια

Δεν γνωρίζω να απαντήσω στο ερώτημα όμως αφού το ψάχνετε έτσι και αλλιώς καλό θα ήταν να γραφτείτε στην Kernelnewbies mailing list
[email protected] εκεί απαντάνε σε όλα τα ερωτήματα...
http://lists.kernelnewbies.org/mailman/listinfo/kernelnewbies

Βασικότερη διαφορά (εκτός των συντακτικών περιορισμών της κάθε επιλογής) είναι ότι όταν καθορίζεις μια συνάρτηση με το #define στην ουσία λες στον preprocessor να αντικαταστήσει όπου βλέπει το όνομα της #defined συνάρτησης με την συνάρτηση. Αντίθετα, όταν ορίζεις μια συνάρτηση inline (αφαιρείται κατόπιν ορθής παρατήρησης του gnulabis) τότε όπου κληθεί η συνάρτηση στον υπόλοιπο κώδικα, στην ουσία το πρόγραμμα την αναζητά και την διαβάζει κάθε φορά που καλείται στο σημείο που έχει γίνει compiled (προσπαθώ να σου το εξηγήσω όσο πιο απλά μπορώ). Π.χ.:

Όταν γράφεις:

#define CUBE(a) a*a*a
int a = 2;
int i = cube(++a);

ο preprocessor θα το μετατρέψει σε

int a = 2;
int i = (++a)*(++a)*(++a);

πριν το compile. Αντίθετα το:

int function cube(a) { return a*a*a }
int a =2;
int i = cube(++a);

θα παραμείνει ως έχει στον κώδικα.

Είναι πιο γρήγορο τόσο κατά την συγγραφή του κώδικα όσο και κατά την εκτέλεση, καθώς γράφεις μια φορά την συνάρτηση, αλλά είναι σαν να την έχεις γράψει ολόκληρη σε κάθε σημείο την καλείς, αφού κατά το precompile η #define συνάρτηση αντικαθιστάται στο σημείο που την καλείς. Αντίστοιχα βέβαια αποκτάς ένα μεγαλύτερο σε μέγεθος εκτελέσιμο...

Ελπίζω να έγινα κατανοητός...

καλά εγώ που είμαι ασχετος το πιασα καπως.. απλα θελει και λιγο της  αντιστοιχης γλωσσας προγραμματισμού

Ευχαριστώ apkoutsou για το χρόνο σου, αλλα ίσως δεν καταλαβες τι ακριβώς ρωταω.
Τα #define σε έναν πηγαιο κώδικα τον κάνουν πιο γρήγορο;
Αν, όχι γιατί ο πηγαίος του linux ειναι γεμάτος #define;

Προφανώς δεν διάβασες καλά την τελευταία παράγραφο...

apkoutsou]Είναι πιο γρήγορο τόσο κατά την συγγραφή του κώδικα όσο και κατά την
εκτέλεση, καθώς γράφεις μια φορά την συνάρτηση, αλλά είναι σαν να την
έχεις γράψει ολόκληρη σε κάθε σημείο την καλείς, αφού κατά το precompile
η #define συνάρτηση αντικαθιστάται στο σημείο που την καλείς
.
Αντίστοιχα βέβαια αποκτάς ένα μεγαλύτερο σε μέγεθος εκτελέσιμο...

Ο πηγαίος κώδικας δεν εκτελείτε, αλλά συμπιλίζεται (μα δεν είναι πολύ ωραία η λέξη που έχουν επιλέξει για το compile ???), οπότε είναι αδόκιμη η φράση εάν "ο πηγαίος κώδικας γίνεται πιο γρήγορος" (εκτός και εάν μιλάς για γλώσσα που έχει interpreter -π.χ. pascal, logo κ.τ.λ. οι οποίες όμως και όπως είναι λογικό δεν έχουν αντίστοιχους ορισμούς- και όχι για compiler -όπως εδώ η c/c++). Δεν γίνεται λοιπόν ο πηγαίος κώδικας πιο γρήγορος (μόνο η συγγραφή του εν προκειμένω πιο γρήγορη - αφού με την #define δεν χρειάζεται να γράφεις ξανά και ξανά τον ίδιο κώδικα, αλλά λές στον preprossecor όπου βλέπει το όρισμα της #define να αντικαθιστά μόνος του τον κώδικα), αλλά το εκτελέσιμο (compiled) αρχείο γίνεται πιο γρήγορο, αλλά ταυτόχρονα και πιο μεγάλο σε μέγεθος. Θα προασπαθήσω να στο εξηγήσω λίγο διαφορετικά. Στα παραδείγματα που σου έδωσα πιο πάνω δες τα βήματα που ακολουθεί η εκτέλεση κάθε κώδικα:

Στο πρώτο παράδειγμα:

Τον κώδικα:

1 #define CUBE(a) a*a*a
2. int a = 2;

3. int b = 3;
4. int i = CUBE(++a);
5. int x = CUBE(b);

ο preprocessor θα τον μετατρέψει σε:

1. int a = 2;
2. int b = 3;
3. int i = (++a)*(++a)*(++a);
4. int x = b*b*b;

πριν το compile. Το οποίο σημαίνει:

1. όρισε ακέραιο a ισο με 2
1. όρισε ακέραιο b ισο με 3
3. όρισε ακέραιο i ίσο με (++a)*(++a)*(++a)
4.
όρισε ακέραιο x ίσο με b*b*b

 

Στο δεύτερο παράδειγμα:

Ο κώδικας:

1. int function cube(a) { return a*a*a }
2. int a = 2;
3. int b = 3;
4. int i = cube(++a);
5. int x = cube(b);

που θα μείνει ως έχει πριν το compile, κατά την εκτέλεση θα τρέξει ως εξής:

1. όρισε συνάρτηση cube()
2. όρισε ακέραιο a ισο με 2
3. όρισε ακέραιο b ισο με 3
4. πήγαινε στο βήμα 1
5. τρέξε συνάρτηση cube(++a)
6. πήγαινε σε βήμα 5
7. όρισε ακέραιο i ίσο με αποτέλεσμα συνάρτησης cube(++a)
8. πήγαινε στο βήμα 1
9. τρέξε συνάρτηση cube(b)
10. πήγαινε στο βήμα 6
11. όρισε ακέραιο x ίσο με αποτέλεσμα συνάρτησης cube(b)

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

Όταν κάποτε είχα την ίδια απορία έψαξα στο internet για "macros vs inline functions". Νομίζω ότι αυτό που είχα διαβάσει τότε ήταν:

http://www.codeproject.com/KB/cpp/Macros_vs_Inlines.aspx

Τα λέει αρκετά καλά, δείχνοντας τόσο τα πλεονεκτήματα όσο και τα μειονεκτήματα των δύο τρόπων.

@apkoutsou: ενώ ξεκίνησες να μιλάς (σωστά) για inline συναρτήσεις, στο 2ο παράδειγμα μιλάς για "κανονικές" συναρτήσεις, που κάνουν δηλαδή jump (και σώζουν stack, κλπ κλπ). Αυτό είναι λίγο άδικο για τη σύγκριση ;)

Ευχαριστω πολυ κατ αρχην. Smile
Λοιπόν από ότι κατάλαβα υπάρχει διαφορά ανάμεσα στο #define και το inline (είχα την εντύπωση πως βγάζουν και τα δύο σχεδόν ίδιο εκτελέσιμο).

ΥΓ όταν ελεγα γρηγοροτερο πηγαιο εννοοθσα εκτελεσιμο, σωστος Wink

@gnulabis: Έχεις απόλυτο δίκιο, απλά στην προσπάθειά μου να το εξηγήσω όσο πιο απλά μπορώ μου ξέφυγε η ορθή ορολογία και από ότι φαίνεται από το τελευταίο post του nilos, πρέπει να γίνει επεξήγηση.

Οπότε nilos διάβασε το άρθρο που προτείνει ο gnulabis διότι είναι πόλυ καλό, πλήρες και αρκετά επεξηγηματικό.

Αυτό που μπορώ να σου είναι ότι σημαντική διαφορά (ΠΑΝΤΑ μη υπολογίζοντας τους περιορισμούς στην σύνταξη κάθε επιλογής τους οποίους μπορείς να δεις στο παραπάνω άρθρο) υπάρχει μόνο μεταξύ #define και κανονικής συνάρτησης (όχι inline συνάρτησης), αφού σε αυτή την περίπτωση ισχύουν όσα έγραψα στην τελευταία μου ανάρτηση.

Η διαφορά τώρα της #define με την inline συνάρτηση είναι ότι η αντικατάσταση του ορίσματος με την συνάρτηση γίνεται σε διαφορετικό χρόνο: στην #define πριν το compile, ενώ στην inline συνάρτηση κατά την εκτέλεση (runtime) του προγράμματος, με αποτέλεσμα καίτοι και οι δύο δίνουν το ίδιο σχεδόν ταχύ πρόγραμμα, εντούτοις οι inline συναρτήσεις να δίνουν μικρότερο σε μέγεθος εκτελέσιμο, αλλά να απαιτούν περισσότερη μνήμη κατά την εκτέλεση, ενώ αντίθετα οι #define συναρτήσεις να δίνουν μεγαλίτερο σε μέγεθος εκτελέσιμο, αλλά να απαιτούν λιγότερη μνήμη κατά την εκτέλεση.

Αντίστοιχα οι κανονικές συναρτήσεις δίνουν ίδιο μέγεθος εκτελέσιμου με τις inline, αλλά απαιτούν περισσότερη μνήμη και φυσικά είναι πιο αργές κατά την εκτέλεση του προγράμματος...

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

Υ.Γ.: Πάντως nilos πήγες και έπιασες ένα πολύ ειδικό και προχωρημένο θέμα της c/c++ ...!!! Για να γράψεις κώδικα με #define συναρτήσεις πρέπει να ξέρεις πολύ καλά τι κάνεις, διότι της εκτέλεσης συνάρτησης που ορίζεται με #define δεν προηγείται έλεγχος, όπως στις κανονικές, με αποτέλεσμα να καταλήγεις πολλές φορές σε παράδοξα ή απλώς λανθασμένα αποτελέσματα...

apkoutsou]@gnulabis:Η διαφορά τώρα της #define με την inline συνάρτηση είναι ότι η αντικατάσταση του ορίσματος με την συνάρτηση γίνεται σε διαφορετικό χρόνο: στην #define πριν το compile, ενώ στην inline συνάρτηση κατά την εκτέλεση (runtime) του προγράμματος, με αποτέλεσμα καίτοι και οι δύο δίνουν το ίδιο σχεδόν ταχύ πρόγραμμα, εντούτοις οι inline συναρτήσεις να δίνουν μικρότερο σε μέγεθος εκτελέσιμο, αλλά να απαιτούν περισσότερη μνήμη κατά την εκτέλεση, ενώ αντίθετα οι #define συναρτήσεις να δίνουν μεγαλίτερο σε μέγεθος εκτελέσιμο, αλλά να απαιτούν λιγότερη μνήμη κατά την εκτέλεση.

Αντίστοιχα οι κανονικές συναρτήσεις δίνουν ίδιο μέγεθος εκτελέσιμου με τις inline, αλλά απαιτούν περισσότερη μνήμη και φυσικά είναι πιο αργές κατά την εκτέλεση του προγράμματος...

Θα διαφωνήσω εδώ. Οι inline έχουν το ίδιο αποτέλεσμα με τα macros (#define): κατά τη διάρκεια του compile, το "σώμα" της συνάρτησης αντικαθιστά την όποια κλήση προς αυτή τη συνάρτηση.

Η κύρια διαφορά είναι ότι τα #define είναι pre-processor directives που σημαίνει ότι αντικαθίστανται νωρίτερα (από τον pre-processor), σε αντίθεση με τις inline συναρτήσεις που τις αναλαμβάνει ο compiler, οπότε η αντικατάσταση γίνεται αργότερα (αλλά πάντα κατά τη διαδικασία της μεταγλώττισης). Αυτό έχει ως αποτέλεσμα:

1) Ο ίδιος κώδικας, με #define κάνει compile πιο γρήγορα από ότι με inlines (μάλλον αυτός είναι ο λόγος που ο πυρήνας είναι γεμάτος με #define). Φυσικά αυτό δεν σημαίνει ότι τρέχει και πιο γρήγορα.

2) Τα #define υπόκεινται σε λιγότερους έως καθόλου ελέγχους, αφού ο compiler δεν τα "βλέπει" ποτέ, η αντικατάσταση έχει γίνει ήδη. Αυτό είναι επικίνδυνο, αλλά και χρήσιμο μερικές φορές αν κανείς το χρησιμοποιεί συνειδητά.

3) Τα #define δεν μπορούν να έχουν το ίδιο περίπλοκο κώδικα όπως μια συνάρτηση

4) Τα #define θα αντικατασταθούν οπωσδήποτε από τον pre-processor, ενώ τα inline μόνο αν κρίνει ο compiler ότι είναι ασφαλές/σωστό να γίνει κάτι τέτοιο. Πχ, αν η inline συνάρτηση έχει κάποιο loop, τότε ο compiler αγνοεί το "inline" και φτιάχνει κανονική συνάρτηση.

Αυτά μπορώ να σκεφτώ. Δείτε και εδώ:
http://en.wikipedia.org/wiki/Inline_function
http://en.wikipedia.org/wiki/Inline_expansion

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

Μεγαλύτερα εκτελέσιμα είναι δυνατόν (αν και σχετικά απίθανο και πάντα ανάλογα την αρχιτεκτονική, το απόλυτο μέγεθος του εκτελέσιμου και τη φύση της δουλειάς που εκτελεί το πρόγραμμα) να τρέξουν τελικά πιο αργά από μικρότερα εκτελέσιμα με περισσότερα instructions. Ο λόγος είναι ότι μεγαλύτερα αρχεία οδηγούν σε μεγαλύτερες απαιτήσεις μνήμης, περισσότερα "πάρε-δώσε" με τη μνήμη, και περισσότερα cache miss. Όπως πάντα, κάτι κερδίζεις, κάτι χάνεις ;)

gnulabis]Η κύρια διαφορά είναι ότι τα #define είναι pre-processor directives που
σημαίνει ότι αντικαθίστανται νωρίτερα (από τον pre-processor), σε
αντίθεση με τις inline συναρτήσεις που τις αναλαμβάνει ο compiler, οπότε
η αντικατάσταση γίνεται αργότερα (αλλά πάντα κατά τη διαδικασία της
μεταγλώττισης). Αυτό έχει ως αποτέλεσμα:

Σωστό, δικό μου το λάθος, βιάστηκα να απαντήσω..

btw, μήπως να συγκεντρώναμε τις παραπάνω πληροφορίες σε ένα mini οδηγό?