8.3 זכרון מטמון תרגול
תרגול - זכרון מטמון¶
תרגיל 1 - חישוב פרמטרי מטמון¶
נתון מטמון L1 עם הפרמטרים הבאים:
- גודל כולל: 32 KB
- גודל שורה: 64 בתים
- אסוציאטיביות: 8-way set-associative
- כתובות של 48 ביט
- חשבו:
- כמה שורות מטמון יש בסה"כ?
- כמה sets יש?
- כמה ביטים ל-offset?
- כמה ביטים ל-index?
-
כמה ביטים ל-tag?
-
נתונה הכתובת (בהקסדצימלי):
0x00007F1234ABCD40 - מהו הoffset?
- מהו הindex (מספר הset)?
-
מהו הtag?
-
נתונות שתי כתובות:
0x0000000000001000ו-0x0000000000002000. האם הן ממופות לאותו set? הסבירו.
תרגיל 2 - חיזוי התנהגות מטמון¶
נתון מטמון direct-mapped (1-way) עם:
- 4 שורות (4 sets)
- גודל שורה: 16 בתים
- כתובות של 16 ביט
חלוקת כתובת: tag (8 ביט) | index (2 ביט) | offset (4 ביט)
נתונה סדרת גישות לכתובות (בדצימלי): 0, 16, 32, 48, 0, 64, 0, 16
- מלאו טבלה עבור כל גישה:
| גישה | כתובת | tag | index | offset | Hit/Miss | מצב הset אחרי |
|---|---|---|---|---|---|---|
| 1 | 0 | 0x00 | 0 | 0 | ? | ? |
| 2 | 16 | ? | ? | ? | ? | ? |
| ... |
- כמה hits וכמה misses?
- שימו לב שכתובות 0 ו-64 ממופות לאותו set. מה קורה? איך מטמון 2-way set-associative היה פותר את הבעיה?
תרגיל 3 - אופטימיזציה לcache¶
נתונות שתי גרסאות של קוד שמאפס מטריצה:
גרסה A - לפי שורות:
#define N 1024
int matrix[N][N];
// גרסה A: i אחרי j (שורה-שורה)
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
matrix[i][j] = 0;
גרסה B - לפי עמודות:
// גרסה B: j אחרי i (עמודה-עמודה)
for (int j = 0; j < N; j++)
for (int i = 0; i < N; i++)
matrix[i][j] = 0;
-
ב-C, מטריצה
int matrix[N][N]מאוחסנת ב-row-major order - כלומרmatrix[0][0],matrix[0][1], ...,matrix[0][1023],matrix[1][0],matrix[1][1], ... ציירו (סכמטית) את סדר הגישה לזכרון של כל גרסה. -
חשבו את שיעור ה-cache miss עבור L1D (32KB, שורה 64B):
- גרסה A: כמה misses מתוך 1024x1024 = 1,048,576 גישות?
-
גרסה B: כמה misses?
(רמז: int הוא 4 בתים, שורת מטמון 64 בתים = 16 int-ים) -
כתבו תוכנית שמודדת את זמן הריצה של שתי הגרסאות (עם N=4096 להבדל ברור). מה היחס?
-
(בונוס) הציעו שיטה לאפס את המטריצה שהיא אפילו יותר מהירה מגרסה A.
תרגיל 4 - ניתוח false sharing¶
נתון הקוד הבא שמריץ שני threads, כל אחד סופר עד 100 מיליון:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
struct shared_data {
long counter0;
long counter1;
};
struct padded_data {
long counter0;
char pad[56]; // padding to separate cache lines
long counter1;
};
void *increment(void *arg) {
long *counter = (long *)arg;
for (long i = 0; i < 100000000L; i++) {
(*counter)++;
}
return NULL;
}
int main() {
struct timespec start, end;
pthread_t t1, t2;
// --- ניסוי 1: בלי padding (false sharing) ---
struct shared_data shared = {0, 0};
clock_gettime(CLOCK_MONOTONIC, &start);
pthread_create(&t1, NULL, increment, &shared.counter0);
pthread_create(&t2, NULL, increment, &shared.counter1);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
clock_gettime(CLOCK_MONOTONIC, &end);
double t_shared = (end.tv_sec - start.tv_sec)
+ (end.tv_nsec - start.tv_nsec) / 1e9;
// --- ניסוי 2: עם padding (בלי false sharing) ---
struct padded_data padded = {0, .counter1 = 0};
clock_gettime(CLOCK_MONOTONIC, &start);
pthread_create(&t1, NULL, increment, &padded.counter0);
pthread_create(&t2, NULL, increment, &padded.counter1);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
clock_gettime(CLOCK_MONOTONIC, &end);
double t_padded = (end.tv_sec - start.tv_sec)
+ (end.tv_nsec - start.tv_nsec) / 1e9;
printf("shared (false sharing): %.3f sec\n", t_shared);
printf("padded (no sharing): %.3f sec\n", t_padded);
printf("ratio: %.2fx\n", t_shared / t_padded);
return 0;
}
-
קמפלו עם
gcc -O2 -pthread -o false_sharing false_sharing.cוהריצו. מה היחס? -
הסבירו בדיוק מה קורה בניסוי 1 (בלי padding):
- על איזה שורת מטמון נמצאים counter0 ו-counter1?
- מה קורה כשthread 0 כותב ל-counter0?
-
מה thread 1 צריך לעשות כדי לכתוב ל-counter1?
-
הסבירו למה ניסוי 2 (עם padding) מהיר יותר.
-
מה היה קורה אם היינו מריצים את שני הthreads על אותה ליבה (באמצעות
taskset -c 0 ./false_sharing)? האם ההבדל בין shared ל-padded היה גדול? קטן? נעלם? הסבירו.