Pointer aliasing terjadi ketika dua atau lebih pointer merujuk ke region memori yang sama. Aliasing bukan bug tersendiri—banyak algoritma yang sengaja memakainya—tetapi ia memengaruhi kemampuan compiler untuk mengoptimalkan kode. Jika compiler tidak yakin apakah dua pointer alias, ia harus mengasumsikan keduanya mungkin alias dan menjaga urutan baca/tulis dengan hati-hati. Keyword restrict (sejak C99) memberi programmer cara untuk berjanji kepada compiler bahwa pointer tertentu tidak alias, membuka peluang optimisasi lebih agresif. Pada chapter ini kita akan memahami aliasing secara konkret dan melihat kapan restrict relevan.

Apa Itu Aliasing #

Dua pointer alias ketika dereference keduanya mengakses region memori yang sama. Contoh paling sederhana: dua pointer int *p1 = &z; int *p2 = &z; keduanya menunjuk ke z—mereka alias. Modifikasi lewat *p1 = 99 otomatis terlihat saat kita membaca *p2 karena keduanya menunjuk memori yang sama. Aliasing juga bisa terjadi antar parameter fungsi: jika pemanggil meneruskan &x sebagai dua argumen, kedua parameter fungsi menjadi alias.

Mekanisme kerjanya: saat compiler menganalisis fungsi void f(int *a, int *b), compiler tidak tahu apakah a dan b alias. Untuk operasi seperti *a += *b; *b += *a;, compiler harus menjaga urutan evaluasi dengan hati-hati karena perubahan *a di baris pertama mempengaruhi nilai *b jika keduanya alias. Compiler tidak bisa, misalnya, memparalelkan dua operasi atau menyimpan *b di register sebagai cache agresif—jika *a dan *b ternyata alias, cache menjadi tidak konsisten.

c
#include <stdio.h>

/* Dua pointer ke region yang sama - aliasing */
void update_saling(int *a, int *b) {
    *a += *b;
    *b += *a;   /* b sekarang = a + b lama + a + b = 2a + 2b */
}

int main(void) {
    int x = 5, y = 3;
    printf("Sebelum: x=%d, y=%d\n", x, y);
    update_saling(&x, &y);
    printf("Setelah : x=%d, y=%d\n", x, y);

    /* Sekarang pakai pointer ke memori sama */
    int z = 10;
    int *p1 = &z;
    int *p2 = &z;
    printf("\nz=%d, *p1=%d, *p2=%d (semua sama)\n", z, *p1, *p2);

    *p1 = 99;
    printf("Setelah *p1=99: z=%d, *p2=%d (aliasing!)\n", z, *p2);

    return 0;
}

Output yang Dihasilkan:

text
Sebelum: x=5, y=3
Setelah : x=8, y=11

z=10, *p1=10, *p2=10 (semua sama)
Setelah *p1=99: z=99, *p2=99 (aliasing!)

Pada update_saling(&x, &y), hasil x=8, y=11 mencerminkan urutan: *a += *b membuat x menjadi 5+3=8; lalu *b += *a membuat y menjadi 3+8=11. Jika a dan b alias (yaitu pemanggil meneruskan &x dua kali), urutan ini menghasilkan jawaban berbeda dan bug halus. Untuk demonstrasi kedua, p1 dan p2 keduanya menunjuk ke z; saat *p1 = 99 dieksekusi, nilai z berubah menjadi 99 dan *p2 juga membaca 99—bukti bahwa keduanya alias.

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-7 (void update_saling(int *a, int *b) { *a += *b; *b += *a; }): dua operasi berurutan. Hasil tergantung pada apakah a dan b alias atau tidak.
  2. Baris 10-13 (update_saling(&x, &y); ...): pemanggilan dengan dua alamat berbeda. Hasil x=8, y=11 sesuai perhitungan non-aliasing.
  3. Baris 16-17 (int *p1 = &z; int *p2 = &z;): dua pointer eksplisit menunjuk ke variabel yang sama. Inilah aliasing dalam bentuk paling sederhana.
  4. Baris 20-21 (*p1 = 99; printf("... *p2 = %d", *p2);): modifikasi lewat p1 langsung terlihat lewat p2. Ini konsekuensi langsung aliasing.

Keyword restrict untuk Optimisasi #

C99 memperkenalkan keyword restrict sebagai janji programmer kepada compiler: pointer yang ditandai restrict tidak akan alias dengan pointer lain yang juga restrict dalam scope yang sama. Dengan janji ini, compiler bisa mengoptimalkan lebih agresif—misalnya, meng-cache nilai *b di register karena yakin *a tidak akan mempengaruhi *b. Jika janji dilanggar (pointer ternyata alias), hasilnya undefined behavior.

c
#include <stdio.h>

/* Tanpa restrict: compiler asumsikan a dan b mungkin alias */
void tambah_tanpa_restrict(int *a, const int *b, int n) {
    for (int i = 0; i < n; i++) {
        a[i] += b[i];
    }
}

/* Dengan restrict: programmer berjanji a dan b tidak alias */
void tambah_dengan_restrict(int *restrict a, const int *restrict b, int n) {
    for (int i = 0; i < n; i++) {
        a[i] += b[i];
    }
}

int main(void) {
    int x[3] = {10, 20, 30};
    int y[3] = {1, 2, 3};

    tambah_tanpa_restrict(x, y, 3);
    printf("Setelah tambah_tanpa_restrict: ");
    for (int i = 0; i < 3; i++) printf("%d ", x[i]);
    printf("\n");

    tambah_dengan_restrict(x, y, 3);
    printf("Setelah tambah_dengan_restrict: ");
    for (int i = 0; i < 3; i++) printf("%d ", x[i]);
    printf("\n");

    /* Bug: jika a dan b benar-benar alias, restrict melanggar janji */
    printf("\nCatatan: jika a dan b alias, gunakan restrict = UB\n");
    return 0;
}

Output yang Dihasilkan:

text
Setelah tambah_tanpa_restrict: 11 22 33 
Setelah tambah_dengan_restrict: 12 24 36 

Catatan: jika a dan b alias, gunakan restrict = UB

Output kedua pemanggilan berbeda karena pada pemanggilan kedua, array x sudah berisi 11, 22, 33 dari operasi pertama—jadi x[i] + y[i] menghasilkan 12, 24, 36. Secara semantik, kedua fungsi menghasilkan jawaban yang sama untuk input yang sama; perbedaan hanya pada optimisasi yang bisa dilakukan compiler. Dalam loop panjang, restrict bisa memungkinkan compiler memvektorisasi loop (SIMD) karena compiler tahu a[i] dan b[i] tidak tumpang tindih. Inilah mengapa banyak fungsi di <string.h> standar C99 memakai restrict pada parameter pointer mereka.