PDA

View Full Version : Performance Tip: Επικάλυψη εγγραφής με υπολογισμούς


DarthMoul
28-07-2004, 13:00
Μεταξύ της CPU και του cache υπάρχει ένας αριθμός από write buffers. Μέσα στα write buffers είναι αποθηκευμένες οι τελευταίες μεταβλητές που έχουν προκαλέσει cache miss. Όσο υπάρχει έστω και μία άδεια buffer, όλα τρέχουν πολύ γρήγορα. Μόλις όμως γεμίσουν ξεκινά μία διαδικασία ενημέρωσης. Όσο διαρκεί η ενημέρωση του cache από τα buffers, κάθε προσπάθεια εγγραφής της μνήμης μπλοκάρετε. Αν όμως έχουμε φροντίσει να κρατήσουμε χρήσιμα πράγματα μέσα στους registers, θα μπορούσαμε να κάνουμε κάποια επεξεργασία, για να μην έχουμε την CPU να κάθεται χάνοντας άδικα τους κύκλους της.

Στους P3 υπάρχουν 12 buffers με μέγεθος 256 bits η κάθε μία. Με άλλα λόγια το κάθε write buffer έχει μέγεθος όσο ένα cache line. Αντίστοιχα οι P4 έχουν 24 write buffers με μέγεθος 512 bits η κάθε μία.

Στους Athlon τώρα έχουμε 12 buffers όπως και στους P3, των οποίων το μέγεθος η AMD μάλλον ξέχασε να γράψει στο manual, αλλά νομίζω πως είναι ασφαλές να υποθέσουμε τα 512bits.

Η πολιτική που ακολουθούν οι Intel και AMD στην ενημέρωση είναι διαφορετική. Η AMD ενημερώνει πάντα το L1 cache. Η Intel το L1 ή το L2 ανάλογα με το που βρήκε το cache miss. Συνεπώς έχουν και διαφορετική συμπεριφορά. Οι Pentium, όταν έχουν βρει cache miss κατά την διάρκεια εγγραφής μεγάλων blocks από δεδομένα, δηλαδή blocks που είναι μεγαλύτερα από το Dcache, θα παρακάμψουν την L1 εντελώς και θα ενημερώσουν απευθείας την L2, κρατώντας την L1 καθαρή από άχρηστα δεδομένα.
Αυτό είναι καλό γιατί μειώνει τα μελλοντικά cache misses στην L1 που είναι πολύ κοντά στην CPU, αλλά σχετικά αργό σαν διαδικασία. Από την άλλη οι Athlon που ενημερώνουν πάντα την L1, είναι γρηγορότεροι, και κατά συνέπεια όταν θα επικαλύψουμε την ενημέρωση του cache με υπολογισμούς, θα είναι μικρότερη η πιθανότητα ο υπολογισμός να τελειώσει πριν την ενημέρωση και να χάσουμε κύκλους μηχανής περιμένοντας. Όμως όταν θα έρθει η ώρα να επεξεργαστούμε ή μετακινήσουμε μεγάλους όγκους δεδομένων, θα υποφέρουμε από τα cache misses και την μείωση του memory bandwidth που προκαλούν. Αυτός είναι και ο λόγος που οι κατασκευαστές databases έβλεπαν πάντα τους Athlon με μισό μάτι. Tα databases για να μειώσουν τις προσπελάσεις του δίσκου που είναι πολύ αργές, κάνουν buffering με όσο μεγαλύτερα blocks μπορούν. Η Intel για να μειώσει την απώλεια χρόνου στην περίπτωση που κάποια από τα δεδομένα που μόλις γράφτηκαν στην L2, χρειαστούν άμεσα στην L1 και προκαλέσουν cache miss, ένωσε την L1 και την L2 με bus 256 bits σε αντίθεση με τα 64 bits του Athlon (και 128 του A64).

Παρακάτω θα κάνουμε και ένα παράδειγμα, για να δούμε πως επικαλύπτουμε το write buffer flush με υπολογισμούς.

DarthMoul
28-07-2004, 18:02
Ας υποθέσουμε ότι σε κάποιο πρόγραμμα μας χρειάζεται να αρχικοποιήσουμε έναν πίνακα από 192 ακεραίους με τιμές από το 4000 έως το 4191 και να υπολογίσουμε το π με ακρίβεια χιλιοστού. Το π το υπολογίζουμε με τους χίλιους πρώτους όρους της σειράς taylor pi/4 = 1 – 1/3 + 1/5 κλπ. To πρόγραμμα είναι φτιαγμένο για έναν Pentium 3 με cache line 32 bytes και 12 write buffers. Πάμε να δούμε πως θα γράφαμε το πρόγραμμα εάν αγνοούσαμε όλα τα παραπάνω:

void wctst()
{
double pi = 0, d;
int a[192], i;

/* Εδώ κάνουμε την αρχικοποίηση. Κάνουμε και loop unrolling
οκτώ φορές ώστε σε κάθε κύκλο του loop να εκμεταλλευόμαστε ολόκληρο το εύρος του cache line */
for (i = 0; i < 192; i = i + 8)
{
a[i + 0] = 4000 + i + 0;
a[i + 1] = 4000 + i + 1;
a[i + 2] = 4000 + i + 2;
a[i + 3] = 4000 + i + 3;
a[i + 4] = 4000 + i + 4;
a[i + 5] = 4000 + i + 5;
a[i + 6] = 4000 + i + 6;
a[i + 7] = 4000 + i + 7;
}

/* και εδώ υπολογίζουμε το π */
for (d = 1; d < 2001; d = d + 4)
{
pi = pi + (1/d);
pi = pi - (1/(d + 2));
}
pi = pi * 4;
}

main()
{
int i;

/* Εδώ εκτελούμε την συνάρτηση μας 100,000 φορές για να γράψει κάτι χρονόμετρο. */
for (i=0; i < 100000; i++)
wctst();
}

Και εδώ είναι η μέτρηση που κάναμε στο παραπάνω:

root@slack:/home/lucast70/scratch# time wctst

real 0m5.083s
user 0m5.100s
sys 0m0.010s

DarthMoul
28-07-2004, 18:05
Και εδώ είναι η βελτιστοποιημένη έκδοση:

void wctst()
{
double pi = 0, d;
int a[192], i;

/* Στην αρχή κάνουμε πάντα κάποιους υπολογισμούς για να προκαλέσουμε το άδειασμα των write buffers στο cache. Δεν μπορούμε να είμαστε σίγουροι ότι αυτό είχε αδειάσει ακριβώς πριν. Εδώ υπολογίζουμε το μισό p/4 */
for (d = 1; d < 2001; d = d + 4)
pi = pi + (1/d);

/* Εδώ γεμίζουμε τα write buffers. Για να γεμίσουν πλήρως χρειάζονται 96 ακεραίους. Κάνουμε λοιπόν την μισή αρχικοποίηση */
for (i = 0; i < 96; i = i + 8)
{
a[i + 0] = 4000 + i + 0;
a[i + 1] = 4000 + i + 1;
a[i + 2] = 4000 + i + 2;
a[i + 3] = 4000 + i + 3;
a[i + 4] = 4000 + i + 4;
a[i + 5] = 4000 + i + 5;
a[i + 6] = 4000 + i + 6;
a[i + 7] = 4000 + i + 7;
}

/* Εδώ υπολογίζουμε το άλλο μισό pi/4. Οι μεταβλητές d και
pi θα βρίσκονται ήδη μέσα στους καταχωρητές από πριν και η διαδικασία δεν θα μπλοκαριστεί από το άδειασμα των write buffers */

for (d = 1; d < 2001; d = d + 4)
pi = pi - (1/(d + 2));

/* Και εδώ κάνουμε την υπόλοιπη αρχικοποίηση */
for (i = 97; i < 192; i = i + 8)
{
a[i + 0] = 4000 + i + 0;
a[i + 1] = 4000 + i + 1;
a[i + 2] = 4000 + i + 2;
a[i + 3] = 4000 + i + 3;
a[i + 4] = 4000 + i + 4;
a[i + 5] = 4000 + i + 5;
a[i + 6] = 4000 + i + 6;
a[i + 7] = 4000 + i + 7;
}

/* Εδώ υπολογίζουμε το pi όσο τα write buffers αδειάζουν */
pi = pi * 4;
}

main()
{
int i;

/* Εδώ εκτελούμε την συνάρτηση μας 100,000 φορές για να γράψει κάτι χρονόμετρο. */

for (i=0; i < 100000; i++)
wctst();
}

Και εδώ έχουμε την μέτρηση:
root@slack:/home/lucast70/scratch# time wctst2

real 0m4.786s
user 0m4.800s
sys 0m0.010s

DarthMoul
28-07-2004, 18:27
Αν προσέξατε στις μετρήσεις ο πραγματικός χρόνος εκτέλεσης που είναι το πεδίο real, είναι μικρότερος από τον χρόνο που αφιέρωσε η cpu στον χρήστη, το πεδίο user δηλαδή. Κανονικά το πεδίο real θα έπρεπε να είναι μεγαλύτερο ή ισο με το άθροισμα sys + user. Μικρότερο είναι πάντα όταν έχουμε επικαλύψεις λειτουργιών ή multithreaded εφαρμογές που κάνουν επικάλυψη προσπέλασης στον δίσκο με υπολογισμούς ή memory movement. Αυτό σημαίνει ότι και στο πρώτο πρόγραμμα είχαμε κάποια επικάλυψη, η οποία όμως ήταν μικρότερη απ'ότι στο δεύτερο, γι αυτό και το δεύτερο μας έδωσε 6% καλύτερους χρόνους.

Cyhaxor
31-12-2004, 23:37
πρέπει να λακουμέ υπόψη και τα ρεύματα που "καταναλονουντε" για να φτάσει στην έξοδο το αποτέλεσμα μας ;)