8.3 זכרון מטמון פתרון
פתרון תרגול - זכרון מטמון¶
1. חישוב פרמטרי מטמון¶
סעיף 1:
- שורות מטמון בסה"כ = גודל כולל / גודל שורה = 32 KB / 64 B = 32,768 / 64 = 512 שורות
- מספר sets = שורות / associativity = 512 / 8 = 64 sets
- ביטים ל-offset = log2(64) = 6 ביטים (כי שורה = 64 בתים)
- ביטים ל-index = log2(64) = 6 ביטים (כי 64 sets)
- ביטים ל-tag = 48 - 6 - 6 = 36 ביטים
סעיף 2:
הכתובת 0x00007F1234ABCD40 בבינארי (48 ביט):
נפצל מימין:
- Offset (6 ביט): 000000 = 0
- Index (6 ביט): 010100 = 20 (set מספר 20)
- Tag (36 ביט): שאר הביטים = 0x00007F1234AB3 (כל מה שנשאר)
ליתר דיוק, נחשב מההקסדצימלי:
- 0x40 = 0100 0000 -> offset = 6 ביט תחתונים = 000000 = 0, הביט ה-7 (01) שייך ל-index
- 0xD40 = 1101 0100 0000 -> offset (6 LSB) = 000000, index (6 ביט הבאים) = 010100 = 20
סעיף 3:
0x0000000000001000 -> offset = 000000 (0), index = bits 6-11 של 0x1000
0x1000 = 0001 0000 0000 0000
ביטים 6-11: 000000 = set 0
0x2000 = 0010 0000 0000 0000
ביטים 6-11: 000000 = set 0
כן, שתי הכתובות ממופות לאותו set (set 0). בשתי הכתובות ביטי הindex הם 0 כי ההבדל ביניהן (0x1000 = 4096 בתים) נמצא בביטים הגבוהים (tag). הכתובות שונות רק ב-tag, אז הן "מתחרות" על אותו set. ב-8-way set-associative שתיהן יכולות להתקיים יחד (יש 8 מקומות ב-set). ב-direct-mapped היתה התנגשות.
2. חיזוי התנהגות מטמון¶
מטמון: 4 sets, direct-mapped, שורה 16B.
כתובת 16 ביט: tag(8) | index(2) | offset(4)
חישוב index = (כתובת >> 4) % 4 = (כתובת / 16) % 4
| כתובת | כתובת/16 | index | tag |
|---|---|---|---|
| 0 | 0 | 0 | 0x00 |
| 16 | 1 | 1 | 0x00 |
| 32 | 2 | 2 | 0x00 |
| 48 | 3 | 3 | 0x00 |
| 64 | 4 | 0 | 0x01 |
טבלת גישות:
| גישה | כתובת | tag | index | Hit/Miss | תוכן set אחרי |
|---|---|---|---|---|---|
| 1 | 0 | 0x00 | 0 | Miss | set0=tag:00 |
| 2 | 16 | 0x00 | 1 | Miss | set1=tag:00 |
| 3 | 32 | 0x00 | 2 | Miss | set2=tag:00 |
| 4 | 48 | 0x00 | 3 | Miss | set3=tag:00 |
| 5 | 0 | 0x00 | 0 | Hit | set0=tag:00 (כבר שם) |
| 6 | 64 | 0x01 | 0 | Miss | set0=tag:01 (דחק את tag:00!) |
| 7 | 0 | 0x00 | 0 | Miss | set0=tag:00 (דחק את tag:01!) |
| 8 | 16 | 0x00 | 1 | Hit | set1=tag:00 (כבר שם) |
סה"כ: 6 misses, 2 hits.
סעיף 3: כתובות 0 ו-64 ממופות לאותו set (set 0) אבל עם tag שונה. בdirect-mapped, כל גישה לאחת דוחקת את השנייה - זו conflict miss. ב-2-way set-associative, שתיהן יכולות להתקיים יחד ב-set 0 (שני "ways"), וגישה 7 הייתה hit במקום miss.
3. אופטימיזציה לcache¶
סעיף 1: סדר הגישה בזכרון:
גרסה A (row-major):
matrix[0][0], matrix[0][1], matrix[0][2], ..., matrix[0][1023],
matrix[1][0], matrix[1][1], ..., matrix[1][1023],
...
גישה רציפה בזכרון.
גרסה B (column-major):
matrix[0][0], matrix[1][0], matrix[2][0], ..., matrix[1023][0],
matrix[0][1], matrix[1][1], ..., matrix[1023][1],
...
כל גישה קופצת 1024 x 4 = 4096 בתים קדימה.
סעיף 2 - חישוב cache misses:
גרסה A:
שורת מטמון = 64B = 16 int-ים. בגישה רציפה, כל 16 גישות יש miss אחד (הראשון). שאר 15 הם hits.
- misses = 1,048,576 / 16 = 65,536 misses
- miss rate = 1/16 = 6.25%
גרסה B:
כל גישה קופצת 4096 בתים = 64 שורות מטמון. כל גישה היא לשורת מטמון אחרת. אם המטריצה גדולה מ-L1 (1024x1024x4 = 4MB >> 32KB), כל גישה מביאה שורה חדשה שדוחקת שורות ישנות. עד שנחזור לאותה שורה (אחרי 1024 גישות), היא כבר נדחקה.
- misses = כמעט 1,048,576 misses (כמעט כל גישה היא miss)
- miss rate = כמעט 100%
היחס: גרסה B איטית פי 16 בערך (או יותר, כי ב-100% miss rate יש גם קנסות נוספים של eviction/writeback).
סעיף 3: תוצאות טיפוסיות עם N=4096:
סעיף 4 - בונוס:
שיטה מהירה יותר - memset:
memset מותאם ל-SIMD ויכול לאפס 64 בתים (שורת מטמון שלמה) במכה אחת עם הוראות vmovaps (AVX). בנוסף, הקומפיילר ומערכת ההפעלה יכולים להשתמש ב-non-temporal stores שעוקפים את הcache (כותבים ישירות ל-RAM) כשכותבים כמויות גדולות - כך לא ממלאים את הcache בנתוני אפסים שלא צריך לקרוא שוב.
4. ניתוח false sharing¶
סעיף 1: תוצאות טיפוסיות:
היחס נע בין 2x ל-10x תלוי במעבד ובמספר הליבות.
סעיף 2: מה קורה בניסוי 1 (בלי padding):
counter0ו-counter1הם שני long-ים (8 בתים כל אחד) צמודים בזכרון. שניהם על אותה שורת מטמון (64 בתים).- כש-thread 0 (רץ על ליבה 0) כותב ל-counter0:
- הליבה 0 צריכה את השורה במצב M (Modified) לפי פרוטוקול MESI
- היא שולחת invalidate לליבה 1
- השורה בcache של ליבה 1 הופכת ל-I (Invalid)
- ליבה 0 כותבת ומסמנת M
- כש-thread 1 (רץ על ליבה 1) רוצה לכתוב ל-counter1:
- השורה בcache שלו במצב I - חייב לבקש אותה מליבה 0
- ליבה 0 שולחת את השורה (write-back) וחוזרת למצב I
- ליבה 1 מקבלת את השורה, מסמנת M, כותבת
- אם ליבה 0 רוצה שוב לכתוב ל-counter0 - חוזרים להתחלה
כל כתיבה גורמת ל-"pingpong" של שורת המטמון בין הליבות. כל transfer כזה לוקח עשרות מחזורים.
סעיף 3: עם padding, counter0 ו-counter1 נמצאים על שורות מטמון שונות (56 בתים ריפוד + 8 בתים counter0 = 64 בתים = שורה שלמה). ליבה 0 כותבת לשורה X וליבה 1 כותבת לשורה Y - אין שום קשר ביניהן. כל ליבה מחזיקה את השורה שלה במצב M בלי להפריע לשנייה.
סעיף 4: אם שני הthreads רצים על אותה ליבה (עם taskset):
- שני הthreads חולקים את אותו L1 cache
- אין תקשורת בין ליבות כי יש רק ליבה אחת
- אין invalidations כי אין ליבה שנייה שמחזיקה עותק
- ההבדל בין shared ל-padded יעלם (או יהיה זניח)
הthread-ים עדיין מתחלפים (context switch) על אותה ליבה, אבל כשthread מקבל את הליבה, השורה כבר במטמון - אין misprediction ואין invalidation.