Προς το περιεχόμενο

C: Περίεργη συμπεριφορά της free()?


geomagas

Προτεινόμενες αναρτήσεις

Κάνεις alloc και zero len + 1 bytes, και μετά κάνεις overwrite τα πρώτα len από αυτά με memcpy.

 

Αντί γι' αυτό, σκέτο alloc len + 1 bytes, overwrite τα πρώτα len από αυτά με memcpy και στο τελευταίο γράφεις 0 με το χέρι.

 

Είναι το ίδιο ακριβώς πράγμα μόνο που τα πρώτα len bytes τα γράφεις μόνο μια φορά αντί για 2 => faster. Το bug δε μπορεί να εμφανιστεί επειδή πάντα len >= 0 άρα το τελευταίο byte (len + 1) όπου γράφεις 0 πάντα θα υπάρχει.

Η calloc δεν είναι απαραίτητα πιο αργή από την malloc. Εξαρτάται βέβαια από το implementation, αλλά ειδικά για μικρά κομμάτια μνήμης (όπως είναι τα strings στην τυπική περίπτωση) τα συγκεντρώνει μέχρι να να γίνει το write κι εκμεταλλεύεται το copy-on-write optimization του λειτουργικού (με απλά λόγια, επιστρέφει ήδη μηδενισμένη μνήμη, που συνήθως είναι κασαρισμένη... κι αν δεν είναι, δημιουργεί(προκαλεί) νέα μηδενισμένη page και "τραβάει" από εκεί).

 

Ειδικά για τα strings, δεν θεωρώ ότι τίθεται θέμα efficiency μεταξύ calloc() και malloc()... τουλάχιστον για την πλειοψηφία των περιπτώσεων. Για μένα, το βασικό κριτήριο για το αν θα χρησιμοποιήσει κανείς calloc() ή malloc() είναι για το αν η initialized μνήμη ενδέχεται να του κρύψει bugs (no immediate crash). Στην προκειμένη περίπτωση όμως, νομίζω δεν τίθεται τέτοιο θέμα.

 

...Να υποθέσω ότι χάσαμε; (άμπαλος εδώ... με την καλή έννοια! :P )

Μόνο 3-0 από την (ο θεός να την κάνει ομάδα) Κολομβία. Μιλάμε δεν βλεπόμασταν!

 

Ώπα sorry, εννοούσα τη λύση με memcpy! :D

Η memcpy είναι all-around... λουκουμάκι για την περίπτωση που θέλεις.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

  • Απαντ. 59
  • Δημ.
  • Τελ. απάντηση

Συχνή συμμετοχή στο θέμα

Συχνή συμμετοχή στο θέμα

#include <string.h>
#include <stdio.h>

int main(void)
{
	char src[10] = "Test";
	char dest[10] = { 50, 50, 50, 50, 50, 50, 50, 50, 50, 50 };
	int i;

	dest[0] = '\0';
	strncat(dest, src, 9);
	printf("Meta apo strncat\n");
	for (i = 0 ; i < 10; i++) {
		printf ("%02d(%c) ", dest[i], dest[i]);
	}
	printf("\n");
	memcpy(dest, src, 10);
	printf("Meta apo memcpy\n");
	for (i = 0 ; i < 10; i++) {
		printf ("%02d(%c) ", dest[i], dest[i]);
	}
	printf("\n");

	return 0;
}
Έξοδος:
Meta apo strncat
84(T) 101(e) 115(s) 116(t) 00() 50(2) 50(2) 50(2) 50(2) 50(2) 
Meta apo memcpy
84(T) 101(e) 115(s) 116(t) 00() 00() 00() 00() 00() 00() 
Δες το παραπάνω χαζό κώδικα. Θέλεις να αντιγράψεις το src στο dest οπότε λες θα βάλω ως len το 10 που έχουν και τα δύο buffers.

 

Ναι αλλά στην περίπτωσή μου, που ξέρω το length που θέλω να αντιγράψω, η memcpy έχει πλεονέκτημα, έτσι;

Αυτό βέβαια, εννοείται, χωρίς να λάβουμε υπόψιν τις υπόλοιπες παραμέτρους που αναφέρεις (περί αρχιτεκτονικών, compilers κλπ).

 

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

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

 

Για τον όγκο των δεδομένων τώρα, το αντικείμενο εδώ είναι (προφανώς) ένας tokenizer. Πράγμα που σημαίνει ότι, αν και τα tokens που θα διαβάζει δεν θα είναι γενικά μεγάλα, μπορεί να είναι πολλά αν το source είναι μεγάλο. Σίγουρα όμως θα επιδέχεται περισσότερες πιο ουσιώδεις βελτιώσεις, μέχρι να φτάσω στο δίλημμα "memcpy ή κάτι άλλο"...

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Ναι αλλά στην περίπτωσή μου, που ξέρω το length που θέλω να αντιγράψω, η memcpy έχει πλεονέκτημα, έτσι;

Αυτό βέβαια, εννοείται, χωρίς να λάβουμε υπόψιν τις υπόλοιπες παραμέτρους που αναφέρεις (περί αρχιτεκτονικών, compilers κλπ).

Η memcpy έχει μειονέκτημα μόνο σε περιπτώσεις όπως τον κώδικα που έδειξα. Φαντάσου πχ ότι έχεις ορίσει ένα πίνακα με μέγεθος 4096 αλλά στη δεδομένη iteration κάποιου for αυτός ο πίνακας περιέχει το string "Kalws tin e8niki...Tria Mhden". Η memcpy θα αντιγράψει 4096 bytes (όχι απαραίτητα. εξαρτάται από το πως δουλεύεις τον κώδικά σου) ενώ η strncat, strlcpy, κτλ θα αντιγράψει 20πόσα bytes είναι το string. Ακόμη και σε αυτή τη περίπτωση όμως δεν νομίζω ότι η διαφορά θα είναι τόσο σημαντική σε σχέση με τον συνολικό χρόνο του προγράμματος. Ένα printf να έχεις πιο πολύ θα καθυστερήσει από αυτό το copy :P

 

Στο πιο σύνηθες φαινόμενο του να ξέρεις από πριν το μέγεθος και να κάνεις malloc με αυτό οπότε το buffer σου κάθε φορά να γεμίζει πλήρως, δεν χάνεις τίποτα να χρησιμοποιήσεις την memcpy, ίσα-ίσα κερδίζεις σε ταχύτητα στις περισσότερες υλοποιήσεις.

 

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

Χωρίς να έχει βαρύτητα η γνώμη μου, εγώ πάντα χρησιμοποιώ την memcpy αντί για τις str* συναρτήσεις. Πολλοί προγραμματιστές λένε ότι οι str* συναρτήσεις είναι πιο readable γιατί δίνουν στον αναγνώστη να καταλάβει ότι γίνεται αντιγραφή string και όχι arbitrary δεδομένων που παραπέμπει η memcpy.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Πολλοί προγραμματιστές λένε ότι οι str* συναρτήσεις είναι πιο readable γιατί δίνουν στον αναγνώστη να καταλάβει ότι γίνεται αντιγραφή string και όχι arbitrary δεδομένων που παραπέμπει η memcpy.

 

Δεν τους αδικώ εκ πρώτης όψεως. Όμως σκέφτομαι ότι αυτά:

memcpy(surname,db_surname,sn_len);
memcpy(email_subject,form_field_2,length);

...μια χαρά readable μου φαίνονται, και όλοι πήραμε ένα hint ότι (μάλλον!) πρόκειται για strings. Αντίθετα, κάτι τέτοιο:

strcat(shebang,chunk);

...θα μου έλεγε ότι, ο "ποιητής" θέλει να συνενώσει "κάτι" με "κάτι" (further investigation required), απλά τον ενδιαφέρει ο χειρισμός του '\0' (δηλαδή ό,τι κι αν είναι τα "κάτια", θέλει να τα χειριστεί σαν strings).

 

Καταλαβαίνω απόλυτα ότι "στη C αν το χειρίζεσαι σαν string τότε αυτόματα είναι string", απλά το αναφέρω για να τονίσω τη σημασία κι άλλων παραγόντων στο readability. Και πάντα με την επιφύλαξη της υποκειμενικότητας, όπως είπαμε παραπάνω και για το elegance με τον migf1.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

...

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

...

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

 

Για τα strings, και λόγω του ότι sizeof(char) == 1 (εκτός από κάτι εξωτικές πλατφόρμες) η δική μου άποψη είναι πως πρέπει να παγιώσεις στο mindset με το οποίο τα δουλεύεις τη διαφορά μεταξύ len και size (λόγω του NUL terminating byte, σε υπαρκτό cstring πρέπει πάντα len < size).

 

Και κατά προτίμηση να δείχνεις την πρόθεσή σου και στον κώδικά σου. Π.χ. για την περίπτωση που συζητάμε, οι 2 παρακάτω κώδικες είναι το ίδιο, αλλά ο 1ος είναι γραμμένος ως len-centric ενώ ο 2ος ως size-centric:

/* --- 1 --- */
size_t len = end - start;     // το len είναι readable hint πως ΔΕΝ υπολογίζεις το NUL terminating byte
char *str = malloc( 1+len );
...

/* --- 2 --- */
size_t sz = 1 + end - start; // το sz είναι readable hint πως υπολογίζεις ΚΑΙ το NUL terminating byte
char *str = malloc( sz );
....
Αν τα έχεις ξεκάθαρα αυτά, τότε έχεις τη δυνατότητα να ελιχθείς ανάλογα με την περίσταση είτε προς τη μια κατεύθυνση είτε προς την άλλη (ασφάλεια/αποδοτικότητα) και να έχεις και αυξημένο readability στον κώδικά σου ως προς τις πραγματικές σου προθέσεις.

 

Αν το έχεις size-centric τότε στη συνέχεια του παραπάνω παραδείγματος (οι τελίτσες δηλαδή) η έκφραση sz-1 σου δίνει ένα readable hint πως εννοείς len. Aν το έχεις len-centric, τότε η έκφραση len+1 σου δίνει ένα readable hint πως εννοείς size.

 

Στη γενική περίπτωση (πέρα δηλαδή από το συγκεκριμένο παράδειγμα) το sz-1 παραπέμπει σε maxlen, και το len παραπέμπει στην 1η κενή (0-based) θέση του string (εκτός αν len+1 == sz, οπότε δεν υπάρχει κενή θέση στο string).

 

Οπότε όταν βλέπεις: memcpy(s1, s2, len) kai memcpy(s1, s2, sz) καταλαβαίνεις πολύ πιο εύκολα τις πραγματικές προθέσεις του συγγραφέα και στην μια και στην άλλη περίπτωση.

 

Από εκεί και πέρα, αν η ασφάλεια είναι το πρώτο σου μέλημα τότε ο "μπούσουλας" πρώτα μηδενίζω sz bytes μνήμη και κατόπιν δουλεύω με len bytes μνήμη είναι μια πάρα πολύ καλή αρχή.

  • Like 1
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Η calloc δεν είναι απαραίτητα πιο αργή από την malloc. Εξαρτάται βέβαια από το implementation, αλλά ειδικά για μικρά κομμάτια μνήμης (όπως είναι τα strings στην τυπική περίπτωση) τα συγκεντρώνει μέχρι να να γίνει το write κι εκμεταλλεύεται το copy-on-write optimization του λειτουργικού (με απλά λόγια, επιστρέφει ήδη μηδενισμένη μνήμη, που συνήθως είναι κασαρισμένη... κι αν δεν είναι, δημιουργεί(προκαλεί) νέα μηδενισμένη page και "τραβάει" από εκεί).

 

Ειδικά για τα strings, δεν θεωρώ ότι τίθεται θέμα efficiency μεταξύ calloc() και malloc()... τουλάχιστον για την πλειοψηφία των περιπτώσεων. Για μένα, το βασικό κριτήριο για το αν θα χρησιμοποιήσει κανείς calloc() ή malloc() είναι για το αν η initialized μνήμη ενδέχεται να του κρύψει bugs (no immediate crash). Στην προκειμένη περίπτωση όμως, νομίζω δεν τίθεται τέτοιο θέμα.

 

Δεν καταλαβαίνω πώς μπορεί να ισχύει αυτό που λες και τι σχέση έχει το COW σε επίπεδο virtual memory, αλλά για να μη μπούμε πάλι σε ανούσιες κόντρες θα σου πώ το πιο απλό:

 

Άσχετα από το πώς παίρνει η calloc τη μνήμη από το λειτουργικό, στη γενική περίπτωση θα σου επιστρέψει μνήμη την οποία μπορεί να έχεις ξαναχρησιμοποιήσει στο παρελθόν με προηγούμενη -alloc και την έχεις επιστρέψει με free. Σ' αυτή την περίπτωση η μνήμη θα έχει τα περιεχόμενα που άφησες την προηγούμενη φορά, οπότε η calloc δε μπορεί να κάνει οτιδήποτε διαφορετικό πέρα από το να τη μηδενίσει manually. Και φυσικά κανένας C allocator που είναι στα καλά του δεν πρόκειται να κρατάει σημειώσεις αν το τάδε τμήμα το έχεις ξαναχρησιμοποιήσει στο παρελθόν για προφανείς λόγους.

 

 

 

Είναι γεγονός ότι την πρώτη φορά που θα ζητήσεις virtual memory allocation από το λειτουργικό πάντα θα πάρεις wiped μνήμη γιατί αν δε γινόταν έτσι θα μπορούσες απλά να διαβάσεις τα προηγούμενα περιεχόμενα, τα οποία πιθανότατα προέρχονται από άλλο process, κι αυτό θα ήταν τεράστιο security problem. Αλλά πρώτον wiped δε σημαίνει απαραίτητα μηδενισμένα όπως εγγυάται η calloc και πολύ περισσότερο όπως είπα παραπάνω δεν πρόκειται κάθε φορά που ζητάς alloc να ζητάει με τη σειρά του το runtime virtual alloc από το λειτουργικό.

 

 

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Δεν είμαι fan της Wikipedia, αλλά στην προκειμένη περίπτωση νομίζω αρκεί για την γενική ιδέα:

http://en.wikipedia.org/wiki/Copy-on-write#Copy-on-write_in_virtual_memory_management

 

Οι λεπτομέρειες εξαρτώνται από το implementation (κράτα όμως αυτό που έγραψα πως για μικρά κομμάτια μνήμης, η aware calloc τα συγκεντρώνει μέχρι να γίνει το 1ο write). Επίσης, αν χρειαστεί να προκαλέσει νέο page, εκεί όντως αργεί πάρα πολύ συγκριτικά με την malloc, αλλά το κάνει μια μόνο φορά και μετά τραβάει απευθείας από την page (προφανώς μέχρι να χρειαστεί να προκαλέσει νέο page).

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Για να μην υπεισέλθω σφήνα στην συζήτηση, μια γενική παρατήρηση..

 

 

Στα Windows πάντως, δυνητικά μια υλοποίηση της calloc θα μπορούσε να εκμεταλλευτεί τις συναρτήσεις του διαχειριστή μνήμη που προσφέρει το ΛΣ ώστε να λάβει άμεσα από αυτόν (δίχως ανάγκη να κάνει η ίδια οτιδήποτε περαιτέρω) ένα zeroed memory block.

 

Τώρα βέβαια τι πέναλτι στην ταχύτητα της πρώτης δέσμευσης μνήμης, μπορεί να επιφέρει ένα αίτημα για zeroed memory block προς τον Memory Manager του ΛΣ μου είναι άγνωστο.

 

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

.....

 

Ναι, ξέρω τι είναι copy on write, ευχαριστώ. Κατά τα άλλα έγραψα κάτι πολύ συγκεκριμένο στο οποίο "άλλα λόγια ν' αγαπιόμαστε". Στη γενική περίπτωση η calloc σου δίνει πίσω μνήμη που έχεις ξαναχρησιμοποιήσει στο παρελθόν, οπότε το τι κάνει ή δεν κάνει το λειτουργικό την πρώτη φορά που σου δίνει ένα virtual page δε μας ενδιαφέρει.

 

Στην προκειμένη μάλιστα περίπτωση του κώδικα του geomagas αυτό ακριβώς συμβαίνει εξώφθαλμα μιας και από εκεί ακριβώς προέρχεται το bug.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

.....

 

Ναι, ξέρω τι είναι copy on write, ευχαριστώ. Κατά τα άλλα έγραψα κάτι πολύ συγκεκριμένο στο οποίο δεν απάντησες. Στη γενική περίπτωση η calloc σου δίνει πίσω μνήμη που έχεις ξαναχρησιμοποιήσει στο παρελθόν, οπότε το τι κάνει ή δεν κάνει το λειτουργικό την πρώτη φορά που σου δίνει ένα virtual page δε μας ενδιαφέρει.

 

To link που σου έδωσα αναφέρει και ειδικά για την calloc. Κι εγώ έγραψα συγκεκριμένα τι γίνεται με μικρά κομμάτια μήμης. Αν δεν με πιστεύεις (πρωτότυπο :P),  ψαξτο περισσότερο.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

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

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

ΟΚ τα γνωστά. FYI δεν πρόκειται να το ψάξω περισσότερο μιας και το γεγονός ότι δεν παίρνω απλή απάντηση σε μια απλή ένσταση μου λέει όλα όσα θέλω να ξέρω.

 

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

@Dx: Έχει  μεγάλο penalty!

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Μπορείς να μου εξηγήσεις πώς γίνεται αυτό το πρόγραμμα να δίνει την έξοδο που δίνει χωρίς να μηδενίζει τη μνήμη manually η calloc?

 

Φυσικά και όχι.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Μπορώ να σου εξηγήσω διάφορα, με την διαφορά πως δεν θέλω. Θεωρώ πως σου έδωσα όσα hints χρειάζονται για να το ψάξεις μόνος σου. Όπως δεν θέλεις εσύ, έτσι δεν θέλω κι εγώ. Όμορφα και σταράτα.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Δημιουργήστε ένα λογαριασμό ή συνδεθείτε για να σχολιάσετε

Πρέπει να είστε μέλος για να αφήσετε σχόλιο

Δημιουργία λογαριασμού

Εγγραφείτε με νέο λογαριασμό στην κοινότητα μας. Είναι πανεύκολο!

Δημιουργία νέου λογαριασμού

Σύνδεση

Έχετε ήδη λογαριασμό; Συνδεθείτε εδώ.

Συνδεθείτε τώρα

  • Δημιουργία νέου...