לדלג לתוכן

9.4 אופטימיזציות של הקומפיילר תרגול

תרגול - אופטימיזציות של הקומפיילר

תרגיל 1 - זיהוי אופטימיזציות

לכל קטע קוד, קמפלו עם -O0 -S ועם -O2 -S והשוו את האסמבלי. זהו אילו אופטימיזציות הקומפיילר ביצע.

קטע א:

int constant_demo() {
    int x = 10;
    int y = 20;
    int z = x + y;
    return z * 2;
}

קטע ב:

int dead_demo() {
    int a = 5;
    int b = a * 3;
    int c = b + 100;
    printf("hello\n");
    return 0;
}

קטע ג:

static int double_it(int x) { return x * 2; }

int inline_demo(int n) {
    return double_it(n) + double_it(n + 1);
}

קטע ד:

void loop_demo(int *arr, int n, int factor) {
    for (int i = 0; i < n; i++) {
        arr[i] = factor * 3 + i;
    }
}

לכל קטע:
1. קמפלו עם gcc -O0 -S -o code_O0.s code.c
2. קמפלו עם gcc -O2 -S -o code_O2.s code.c
3. השוו את האסמבלי וזהו את האופטימיזציות

תרגיל 2 - volatile בפעולה

צרו את הקובץ הבא:

// volatile_test.c
int regular_loop() {
    int flag = 1;
    int count = 0;
    while (flag) {
        count++;
        if (count >= 1000000) {
            flag = 0;
        }
    }
    return count;
}

int volatile_loop() {
    volatile int flag = 1;
    int count = 0;
    while (flag) {
        count++;
        if (count >= 1000000) {
            // flag יכול להשתנות מבחוץ (למשל על ידי signal handler)
            // ב"אמת" היינו בודקים תנאי חיצוני
            flag = 0;
        }
    }
    return count;
}
  1. קמפלו עם -O2 -S ובדקו את האסמבלי של regular_loop. האם הקומפיילר שמר את flag במשתנה?
  2. בדקו את האסמבלי של volatile_loop. מה ההבדל?
  3. הסבירו: למה בלי volatile הקומפיילר יכול לבצע אופטימיזציות שישנו את ההתנהגות?
  4. נסו להוסיף שורה printf("%d\n", flag); בתוך הלולאה של regular_loop. מה משתנה באסמבלי? למה?

תרגיל 3 - השוואת ביצועים בין רמות אופטימיזציה

כתבו תוכנית שמבצעת חישוב כבד וימדדו את הזמן:

// perf.c
#include <stdio.h>
#include <time.h>

#define N 100000000

long long sum_squares(int n) {
    long long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += (long long)i * i;
    }
    return sum;
}

int main() {
    clock_t start = clock();
    long long result = sum_squares(N);
    clock_t end = clock();

    double elapsed = (double)(end - start) / CLOCKS_PER_SEC;
    printf("Result: %lld\n", result);
    printf("Time: %.3f seconds\n", elapsed);
    return 0;
}
  1. קמפלו והריצו ב-4 רמות: -O0, -O1, -O2, -O3. רשמו את הזמן של כל רמה.
  2. השוו את האסמבלי של sum_squares ב--O0 מול -O3. מה ההבדלים?
  3. נסו גם עם -Os (אופטימיזציה לגודל). מה הזמן? מה גודל הקובץ לעומת -O3?
  4. נסו עם -O3 -march=native (שימוש בכל הוראות המעבד המקומי). האם יש שיפור נוסף?

תרגיל 4 - tail call optimization

// tail.c
#include <stdio.h>

// גרסה רגילה (לא tail call)
int factorial_regular(int n) {
    if (n <= 1) return 1;
    return n * factorial_regular(n - 1);  // כפל אחרי הקריאה - לא tail call
}

// גרסה tail-call
int factorial_tail(int n, int acc) {
    if (n <= 1) return acc;
    return factorial_tail(n - 1, n * acc);  // הקריאה היא הדבר האחרון
}
  1. קמפלו עם -O2 -S ובדקו את האסמבלי של שתי הפונקציות.
  2. בגרסה של factorial_tail - האם הקומפיילר הפך את הרקורסיה ללולאה? חפשו call מול jmp.
  3. בגרסה של factorial_regular - האם גם כאן הקומפיילר הצליח לבצע אופטימיזציה?
  4. כתבו תוכנית שקוראת ל-factorial_regular(1000000). מה קורה? (רמז: stack overflow). ואז נסו עם factorial_tail(1000000, 1) ב--O2 - מה קורה?

תרגיל 5 - LTO - אופטימיזציית זמן לינקוג'

צרו שני קבצים:

// math_ops.c
int square(int x) {
    return x * x;
}

int cube(int x) {
    return x * x * x;
}

int unused_function(int x) {
    return x + 42;
}
// main.c
#include <stdio.h>

int square(int x);
int cube(int x);

int main() {
    int a = square(5);
    int b = cube(3);
    printf("square(5)=%d, cube(3)=%d\n", a, b);
    return 0;
}
  1. קמפלו בלי LTO:

    gcc -O2 -c math_ops.c -o math_ops.o
    gcc -O2 -c main.c -o main.o
    gcc -O2 math_ops.o main.o -o no_lto
    

    בדקו עם nm no_lto - האם unused_function נמצא בקובץ? האם square ו-cube הוטמעו?

  2. קמפלו עם LTO:

    gcc -O2 -flto -c math_ops.c -o math_ops_lto.o
    gcc -O2 -flto -c main.c -o main_lto.o
    gcc -O2 -flto math_ops_lto.o main_lto.o -o with_lto
    

    בדקו עם nm with_lto - האם unused_function נמצא? האם square ו-cube הוטמעו?

  3. השוו את גודל שני הקבצים: ls -la no_lto with_lto

  4. בדקו את האסמבלי של main בשני המקרים עם objdump -d. מה ההבדל?