לדלג לתוכן

10.3 טריקים ביטיים פתרון

פתרון - טריקים ביטיים - bitwise tricks

פתרון 1 - מערכת הרשאות עם דגלים

#include <stdio.h>

#define PERM_READ   (1 << 0)  // 0001
#define PERM_WRITE  (1 << 1)  // 0010
#define PERM_EXEC   (1 << 2)  // 0100
#define PERM_ADMIN  (1 << 3)  // 1000

void print_permissions(int perms) {
    printf("permissions: ");
    if (perms & PERM_READ)  printf("READ ");
    if (perms & PERM_WRITE) printf("WRITE ");
    if (perms & PERM_EXEC)  printf("EXEC ");
    if (perms & PERM_ADMIN) printf("ADMIN ");
    printf("\n");
}

int grant(int perms, int flag) {
    return perms | flag;
}

int revoke(int perms, int flag) {
    return perms & ~flag;
}

int has_permission(int perms, int flag) {
    return (perms & flag) != 0;
}

int main(void) {
    int perms = PERM_READ | PERM_WRITE;
    printf("initial:\n");
    print_permissions(perms);

    perms = grant(perms, PERM_EXEC);
    printf("\nafter granting EXEC:\n");
    print_permissions(perms);

    perms = revoke(perms, PERM_WRITE);
    printf("\nafter revoking WRITE:\n");
    print_permissions(perms);

    printf("\nhas READ? %s\n", has_permission(perms, PERM_READ) ? "yes" : "no");
    printf("has WRITE? %s\n", has_permission(perms, PERM_WRITE) ? "yes" : "no");
    printf("has EXEC? %s\n", has_permission(perms, PERM_EXEC) ? "yes" : "no");

    return 0;
}

פלט:

initial:
permissions: READ WRITE

after granting EXEC:
permissions: READ WRITE EXEC

after revoking WRITE:
permissions: READ EXEC

has READ? yes
has WRITE? no
has EXEC? yes


פתרון 2 - ספירת ביטים

#include <stdio.h>
#include <time.h>

int popcount_naive(unsigned int n) {
    int count = 0;
    for (int i = 0; i < 32; i++) {
        count += (n >> i) & 1;
    }
    return count;
}

int popcount_kernighan(unsigned int n) {
    int count = 0;
    while (n) {
        n &= n - 1;
        count++;
    }
    return count;
}

int popcount_builtin(unsigned int n) {
    return __builtin_popcount(n);
}

int main(void) {
    unsigned int test_values[] = {0, 1, 255, 0xFFFFFFFF, 0xAAAAAAAA};
    int num_tests = sizeof(test_values) / sizeof(test_values[0]);

    for (int i = 0; i < num_tests; i++) {
        unsigned int n = test_values[i];
        printf("popcount(0x%08X): naive=%d, kernighan=%d, builtin=%d\n",
               n,
               popcount_naive(n),
               popcount_kernighan(n),
               popcount_builtin(n));
    }

    // השוואת ביצועים
    printf("\n--- benchmarks (1M iterations) ---\n");
    clock_t start, end;
    volatile int result;

    start = clock();
    for (int i = 0; i < 1000000; i++)
        result = popcount_naive(0xAAAAAAAA);
    end = clock();
    printf("naive:     %ld us\n", (end - start) * 1000000 / CLOCKS_PER_SEC);

    start = clock();
    for (int i = 0; i < 1000000; i++)
        result = popcount_kernighan(0xAAAAAAAA);
    end = clock();
    printf("kernighan: %ld us\n", (end - start) * 1000000 / CLOCKS_PER_SEC);

    start = clock();
    for (int i = 0; i < 1000000; i++)
        result = popcount_builtin(0xAAAAAAAA);
    end = clock();
    printf("builtin:   %ld us\n", (end - start) * 1000000 / CLOCKS_PER_SEC);

    return 0;
}

הערך 0xAAAAAAAA בבינארי הוא 10101010... - 16 ביטים דלוקים מתוך 32. אלגוריתם Kernighan יבצע 16 איטרציות, הגרסה הנאיבית תמיד 32, ו-builtin פקודה בודדת.


פתרון 3 - חזקת 2

#include <stdio.h>

int is_power_of_2(unsigned int n) {
    return n && !(n & (n - 1));
}

unsigned int next_pow2(unsigned int n) {
    if (n == 0) return 1;
    if (is_power_of_2(n)) return n;
    n--;
    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8;
    n |= n >> 16;
    n++;
    return n;
}

unsigned int prev_pow2(unsigned int n) {
    if (n == 0) return 0;
    // מיקום הביט הגבוה ביותר
    int highest_bit = 31 - __builtin_clz(n);
    return 1u << highest_bit;
}

int main(void) {
    unsigned int values[] = {0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 100, 128, 255, 256};
    int n = sizeof(values) / sizeof(values[0]);

    printf("%-6s %-10s %-10s %-10s\n", "n", "is_pow2", "next_pow2", "prev_pow2");
    printf("--------------------------------------\n");

    for (int i = 0; i < n; i++) {
        unsigned int v = values[i];
        printf("%-6u %-10s %-10u %-10u\n",
               v,
               is_power_of_2(v) ? "yes" : "no",
               next_pow2(v),
               prev_pow2(v));
    }

    return 0;
}

פלט (חלקי):

n      is_pow2    next_pow2  prev_pow2
--------------------------------------
0      no         1          0
1      yes        1          1
2      yes        2          2
3      no         4          2
4      yes        4          4
5      no         8          4
100    no         128        64
128    yes        128        128
255    no         256        128
256    yes        256        256

הפונקציה prev_pow2 עובדת כך: __builtin_clz(n) מחזיר את מספר האפסים המובילים. אם n=100 (01100100), יש 25 אפסים מובילים, אז הביט הגבוה הוא 31-25=6. חזקת 2 הקרובה מלמטה היא 1 << 6 = 64.


פתרון 4 - מניפולציות ביטיות

#include <stdio.h>

void print_binary(unsigned int n) {
    for (int i = 31; i >= 0; i--) {
        printf("%d", (n >> i) & 1);
        if (i % 8 == 0) printf(" ");
    }
    printf("\n");
}

unsigned int reverse_bits(unsigned int n) {
    unsigned int result = 0;
    for (int i = 0; i < 32; i++) {
        result <<= 1;
        result |= (n & 1);
        n >>= 1;
    }
    return result;
}

unsigned int extract_bits(unsigned int n, int start, int len) {
    // יוצר מסכה של len ביטים דלוקים
    unsigned int mask = (1u << len) - 1;
    // מזיז ימינה ל-start ומחיל את המסכה
    return (n >> start) & mask;
}

unsigned int set_bits(unsigned int n, int start, int len, unsigned int val) {
    unsigned int mask = (1u << len) - 1;
    // מנקה את הביטים הישנים
    n &= ~(mask << start);
    // מכניס את הערך החדש
    n |= (val & mask) << start;
    return n;
}

int main(void) {
    // reverse_bits
    unsigned int x = 0x80000001; // 10000...0001
    printf("reverse_bits:\n");
    printf("  input:  "); print_binary(x);
    printf("  output: "); print_binary(reverse_bits(x));

    // extract_bits
    printf("\nextract_bits:\n");
    unsigned int val = 0xFF00;
    printf("  extract_bits(0x%X, 8, 4) = 0x%X\n",
           val, extract_bits(val, 8, 4)); // 0xF

    printf("  extract_bits(0x%X, 8, 8) = 0x%X\n",
           val, extract_bits(val, 8, 8)); // 0xFF

    // set_bits
    printf("\nset_bits:\n");
    unsigned int n = 0x0000;
    n = set_bits(n, 4, 4, 0xA); // מכניס 0xA בביטים 4-7
    printf("  set_bits(0, 4, 4, 0xA) = 0x%X\n", n); // 0xA0

    n = set_bits(n, 0, 4, 0x5); // מכניס 0x5 בביטים 0-3
    printf("  set_bits(0xA0, 0, 4, 0x5) = 0x%X\n", n); // 0xA5

    return 0;
}

ההסבר ל-extract_bits: המסכה (1u << len) - 1 יוצרת len ביטים של 1. למשל, (1 << 4) - 1 = 0b1111 = 0xF. אחרי שמזיזים את n ימינה ב-start, מחילים AND עם המסכה כדי לקחת רק את הביטים שרוצים.

ההסבר ל-set_bits: קודם מנקים את הביטים הישנים (AND עם NOT של המסכה המוזזת), ואז מכניסים את הערך החדש (OR עם הערך מוזז).


פתרון 5 - שדות סיביות ו-endianness

#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>

struct ip_header {
    unsigned int version      : 4;
    unsigned int ihl          : 4;
    unsigned int tos          : 8;
    unsigned int total_length : 16;
};

void fill_ip_header(struct ip_header *hdr) {
    hdr->version = 4;
    hdr->ihl = 5;
    hdr->tos = 0;
    hdr->total_length = 60;
}

uint16_t my_htons(uint16_t val) {
    return ((val & 0xFF) << 8) | ((val >> 8) & 0xFF);
}

uint32_t my_htonl(uint32_t val) {
    return ((val & 0x000000FF) << 24) |
           ((val & 0x0000FF00) << 8)  |
           ((val & 0x00FF0000) >> 8)  |
           ((val & 0xFF000000) >> 24);
}

int main(void) {
    // חלק 1-3: bit fields
    struct ip_header hdr;
    fill_ip_header(&hdr);

    printf("IP header:\n");
    printf("  version = %u\n", hdr.version);
    printf("  ihl = %u\n", hdr.ihl);
    printf("  tos = %u\n", hdr.tos);
    printf("  total_length = %u\n", hdr.total_length);
    printf("  sizeof(ip_header) = %lu bytes\n", sizeof(hdr));

    // חלק 4-6: endianness
    printf("\nhtons comparison:\n");
    uint16_t val16 = 0x1234;
    printf("  my_htons(0x%04X) = 0x%04X\n", val16, my_htons(val16));
    printf("  htons(0x%04X)    = 0x%04X\n", val16, htons(val16));

    printf("\nhtonl comparison:\n");
    uint32_t val32 = 0x12345678;
    printf("  my_htonl(0x%08X) = 0x%08X\n", val32, my_htonl(val32));
    printf("  htonl(0x%08X)    = 0x%08X\n", val32, htonl(val32));

    return 0;
}

ההסבר ל-my_htons: לוקחים את הבית הנמוך (val & 0xFF) ומזיזים אותו 8 ביטים שמאלה, ואת הבית הגבוה (val >> 8) מזיזים ימינה. בעצם מחליפים את סדר שני הבתים.

ההסבר ל-my_htonl: אותו עיקרון אבל עם 4 בתים. כל בית עובר למיקום ה"מראה" שלו:
- בית 0 (הנמוך) עובר למיקום 3 (הגבוה)
- בית 1 עובר למיקום 2
- בית 2 עובר למיקום 1
- בית 3 (הגבוה) עובר למיקום 0 (הנמוך)