Try   HackMD

2022q1 Homework3 (quiz3)

contributed by < Tcc0403 >
作業需求
測驗題目

測驗 1

題目

在 Linux 核心原始程式碼,include/linux/bitfield.h 提及一個巨集 GENMASK,其作用是依據給定的範圍,產生連續的 bitmask,例如:

  • GENMASK(6, 4) 產生 011100002
  • GENMASK(39, 21) 產生 0x000000ffffe00000 (64 位元)

已知我們使用的微處理器架構為 64 位元,且 unsigned long 為 8 位元組寬度 (符合 LP64 資料模型),以下是可能的 GENMASK 巨集的定義:

#define GENMASK(h, l) \
    (((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT)))

請補完,使其行為符合預期。作答規範:

  1. LEFTRIGHT 應以最簡潔的形式撰寫,且符合作業一排版規範 (近似 Linux 核心程式碼排版風格)
  2. LEFTRIGHT 皆為表示式,可能包含常數
  3. LEFTRIGHT 皆不該包含小括號 (即 ())

作答

觀察 bitmask 的產生方式為將兩個 ~0UL (即 64 位元皆為 1)進行相應的位移操作後,做 AND 操作,因此我們可以推斷位移操作的目的是將範圍外的位元變為 0 ,以下計算前後兩項所需清除的位元:

  • (~0UL) >> (LEFT): 該項為右移,目的為清除範圍外的高位元,最高位元是第 63 位元,因此需要清除 63-h 個位元,故 LEFT = 63-h
  • (~0UL) >> (l) << (RIGHT): 該項為左移,目的為清除範圍外的低位元,最低位元是第 0 位元,因此需要清除 l - 0 = l 個位元,故 RIGHT = l

(0UL)>>(63h)=0063h11h+1(0UL)>>(l)<<(l)=00l11642l00l

0  063h1        1h+100l11642l00lGENMASK(h, l) =0063h1100l

其中 (~0UL) >> (l) << (RIGHT) 的部分不太清楚為甚麼需要先右移再左移,範圍外的高位元應該在前一項就已經排除了,這邊的右移操作不知道有甚麼考量

答案 :

  • LEFT = 63-h
  • RIGHT = l

測驗 2

題目

考慮以下程式碼:

struct foo;
  
struct fd {
    struct foo *foo;
    unsigned int flags;
};

enum {
    FOO_DEFAULT = 0,
    FOO_ACTION,                           
    FOO_UNLOCK,
} FOO_FLAGS;

static inline struct fd to_fd(unsigned long v)
{
    return (struct fd){EXP1, v & 3};
}

函式 fo_fd 接受指定的地址 v,填入一個 fd 結構體,並確保第一個成員的地址得以依據〈你所不知道的 C 語言:記憶體管理、對齊及硬體特性〉描述,對 4 個位元組進行向下對齊 (即 C++ Boost 描述的 align_down)。其中 struct foo; 運用〈你所不知道的 C 語言:指標篇〉提及的 forward declaration 技巧,可在實作階段才提供具體 struct foo 的定義。

請補完,使其行為符合預期。作答規範:

  1. EXP1 應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息
  2. EXP1 不得使用 % (modulo) 運算子
  3. 當變數和常數進行運算時,變數應該出現前於常數,例如 v | 1

作答

依題目所要求, v 向下對齊到能被 4 整除的地址上,將 v 的 bit 0 跟 bit 1 清除便是 4 的倍數,使用位元遮罩來保留 bit 0, 1 以外的位元,即 v & ~3
接著透過 (struct foo *) 轉型成 foo 結構體的指標存取該地址

答案:

  • EXP1 = (struct foo *)(v & ~3)

測驗 3

題目

考慮以下程式碼,能將輸入的 8 位元無號整數逐一位元反轉,如同 LeetCode 190. Reverse Bits 的描述。

#include <stdint.h>
uint8_t rev8(uint8_t x)
{
    x = (x >> 4) | (x << 4);               
    x = ((x & 0xCC) >> 2) | (EXP2);
    x = ((x & 0xAA) >> 1) | (EXP3);
    return x;
}

請補完,使其行為符合預期。作答規範:

  1. EXP2EXP3 應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息
  2. 當變數和常數進行運算時,變數應該出現前於常數,例如 v | 1

作答

觀察題目,得知該程式碼逐步交換位元,步驟如下:

  • 第一次交換: 將 4 位高位元與 4 位低位元交換,使用位移操作剛好可以將位元移至目標位置






A



original


7


6


5


4


3


2


1


0



swap


3


2


1


0


7


6


5


4



original:port0->swap:port3





original:port1->swap:port2





  • 第二次交換: 2 bits 為一組,相鄰組別兩兩交換,(x & 0xCC) 為圖中灰色的位元,剩餘的白色位元即為 (x & ~0xCC)=(x & 0x33),對應的位移操作為左移 2 位。 EXP2 = (x & 0x33) << 2






A



original


3


2


1


0


7


6


5


4



swap


1


0


3


2


5


4


7


6



original:port0->swap:port5





original:port1->swap:port4





original:port2->swap:port7





original:port3->swap:port6





  • 第三次交換: 1 bit 為一組,相鄰組別兩兩交換,(x & 0xAA) 為圖中灰色的位元,剩餘的白色位元即為 (x & ~0xAA)=(x & 0x55),對應的位移操作為左移 1 位。 EXP3 = (x & 0x55) << 1






A



original


1


0


3


2


5


4


7


6



swap


0


1


2


3


4


5


6


7



original:port0->swap:port9





original:port1->swap:port8





original:port2->swap:port11





original:port3->swap:port10





original:port4->swap:port13





original:port5->swap:port12





original:port6->swap:port15





original:port7->swap:port14





答案 :

  • EXP2 = (x & 0x33) << 2
  • EXP3 = (x & 0x55) << 1

測驗 4

題目

延伸 〈你所不知道的 C 語言:前置處理器應用篇〉,我們嘗試將 foreach 巨集 予以擴充,使得支援以下寫法:

    int i;
    foreach_int(i, 0, -1, 100, 0, -99) {
        printf("i is %i\n", i);
    }
    const char *p;
    foreach_ptr(p, "Hello", "world") {
        printf("p is %s\n", p);
    }

預期輸出如下:

i is 0
i is -1
i is 100
i is 0
i is -99
p is Hello
p is world

對應的巨集定義:

#include <assert.h>
#define _foreach_no_nullval(i, p, arr) \
    assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p))

#define foreach_int(i, ...)                                            \
    for (unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0); \
         _foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int);      \
         (i) = ((int[]){__VA_ARGS__, 0})[EXP4])

#define foreach_ptr(i, ...)                                                 \
    for (unsigned _foreach_i =                                              \
             (((i) = (void *) ((typeof(i)[]){__VA_ARGS__})[0]), 0); \
         (i); (i) = (void *) ((typeof(i)[]){__VA_ARGS__,            \
                                                    NULL})[EXP5],   \
                  _foreach_no_nullval(_foreach_i, i,                        \
                                      ((const void *[]){__VA_ARGS__})))

請補完,使其行為符合預期。作答規範:

  1. EXP4EXP5 應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息
  2. 不該出現小括號 (即 ( 和 ))

作答

題目中 foreach 的功能為走訪第一個除外的所有參數,並將其值指派給第一個參數

foreach_int 巨集中,分析 for (clause-1; exp-2; exp-3) 陳述句中的三個部分:

  • clause-1 : 初始化 i_foreach_i
  • exp-2 : 判斷 _foreach_i 是否超過可走訪大小
  • exp-3 : 更新 i_foreach_i

我們需要補上更新的部分, i 需要更新為下一個參數, (int[]){__VA_ARGS__, 0} 是將欲走訪之參數視作陣列,透過 array subscript 存取下一個元素,利用 ++_foreach_i ,同時更新 _for_each_i。故 EXP4 = ++_foreach_i__VA_ARGS__ 後面補上 0 可以避免因前置遞增運算子而存取到錯誤的記憶體空間

foreach_iptr 巨集中與 foreach_int 類似,走訪各個字串,其中 _foreach_no_nullval 巨集的功能為檢查空字串。 EXP5 的功能與 EXP4 相同,皆是在走訪下一個元素的同時更新作為索引值的變數

答案 :

  • EXP4 = ++_foreach_i
  • EXP5 = ++_foreach_i

測驗 5

題目

針對 LeetCode 29. Divide Two Integers,以下是可能的實作:

#include <limits.h>
int divide(int dividend, int divisor)
{
    int signal = 1;
    unsigned int dvd = dividend;
    if (dividend < 0) {
        signal *= -1;
        dvd = ~dvd + 1;
    }

    unsigned int dvs = divisor;
    if (divisor < 0) {
        signal *= -1;
        dvs = ~dvs + 1;
    }

    int shift = 0;
    while (dvd > (EXP6))
        shift++;

    unsigned int res = 0;
    while (dvd >= dvs) {
        while (dvd < (dvs << shift))
            shift--;                         
        res |= (unsigned int) 1 << shift;
        EXP7;
    }

    if (signal == 1 && res >= INT_MAX)
        return INT_MAX;
    return res * signal;
}

請補完,使其行為符合預期。作答規範:

  1. EXP6EXP7 應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息
  2. 不該出現小括號 (即 ( 和 ))

作答

先觀察 EXP6 所在迴圈的功用,推測為計算除數對齊被除數所需要的最大位移量,當位移後的除數大於被除數便跳出迴圈,故 EXP6 = dvs << shift

再來觀察 EXP7 所在迴圈的功用,推測為計算商數

while (dvd >= dvs) { while (dvd < (dvs << shift)) shift--; res |= (unsigned int) 1 << shift; EXP7; }
  • 第 2 ~ 3 行 : 調整除數的位移量,找出能使被除數減去位移後的除數的對應位移量
  • 第 4 行 : 將 1 填入商數中相對應的位元
  • 第 5 行 : 這邊需要更新被除數,為被除數與位移後的除數兩者之差,故 EXP7 = dvd -= dvs << shift

EXP6 = dvs << shift
EXP7 = dvd -= dvs << shift

測驗 6

題目

針對 LeetCode 149. Max Points on a Line,以下是可能的實作:

#include <stdbool.h>
#include "list.h"

struct Point {
    int x, y;
};

struct point_node {
    int p1, p2;
    struct list_head link;
};

static bool can_insert(struct list_head *head, int p1, int p2)
{
    struct point_node *pn;
    list_for_each_entry (pn, head, link)
        return EXP8;
    return true;
}

static int gcd(int x, int y)
{
    while (y) {
        int tmp = y;
        y = x % y;
        x = tmp;
    }
    return x;
}

static int maxPoints(struct Point *points, int pointsSize)
{
    if (pointsSize <= 2)
        return pointsSize;

    int i, j, slope_size = pointsSize * pointsSize / 2 + 133;
    int *dup_cnts = malloc(pointsSize * sizeof(int));
    int *hori_cnts = malloc(pointsSize * sizeof(int));
    int *vert_cnts = malloc(pointsSize * sizeof(int));
    int *slope_cnts = malloc(slope_size * sizeof(int));
    memset(hori_cnts, 0, pointsSize * sizeof(int));
    memset(vert_cnts, 0, pointsSize * sizeof(int));
    memset(slope_cnts, 0, slope_size * sizeof(int));

    for (i = 0; i < pointsSize; i++)
        dup_cnts[i] = 1;

    struct list_head *heads = malloc(slope_size * sizeof(*heads));
    for (i = 0; i < slope_size; i++)
        INIT_LIST_HEAD(&heads[i]);

    for (i = 0; i < pointsSize; i++) {
        for (j = i + 1; j < pointsSize; j++) {
            if (points[i].x == points[j].x)
                hori_cnts[i]++, hori_cnts[j]++;

            if (points[i].y == points[j].y)
                vert_cnts[i]++, vert_cnts[j]++;

            if (points[i].x == points[j].x && points[i].y == points[j].y)
                dup_cnts[i]++, dup_cnts[j]++;

            if (points[i].x != points[j].x && points[i].y != points[j].y) {
                int dx = points[j].x - points[i].x;
                int dy = points[j].y - points[i].y;
                int tmp = gcd(dx, dy);
                dx /= tmp;
                dy /= tmp;
                int hash = dx * dy - 1333 * (dx + dy);
                if (hash < 0)
                    hash = -hash;
                hash %= slope_size;
                if (can_insert(&heads[hash], i, j)) {
                    struct point_node *pn = malloc(sizeof(*pn));
                    pn->p1 = i;
                    pn->p2 = j;
                    EXP9;
                }
            }
        }
    }

    for (i = 0; i < slope_size; i++) {
        int index = -1;
        struct point_node *pn;
        list_for_each_entry (pn, &heads[i], link) {
            index = pn->p1;
            slope_cnts[i]++;
        }
        if (index >= 0)
            slope_cnts[i] += dup_cnts[index];
    }

    int max_num = 0;
    for (i = 0; i < pointsSize; i++) {
        if (hori_cnts[i] + 1 > max_num)
            max_num = hori_cnts[i] + 1;
        if (vert_cnts[i] + 1 > max_num)
            max_num = vert_cnts[i] + 1;
    }
    for (i = 0; i < slope_size; i++) {
        if (slope_cnts[i] > max_num)
            max_num = slope_cnts[i];
    }

    return max_num;
}

請補完,使其行為符合預期。作答規範:

  1. EXP8EXP9 應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息
  2. EXP8 不該出現小括號 (即 ())
  3. EXP9 為包含 list_ 開頭巨集的單一敘述

作答

根據 EXP8 所在的函式名稱,猜測該函式的功能為判斷座標 (p1, p2) 是否符合某種條件,可以插入該位置的鏈結串列,觀察尋找呼叫該函式的程式碼段落,判斷此條件為

測驗 7

題目

考慮 ilog32 函式可針對給定的 32 位元無號數,計算扣除開頭的 0,最少需要多少位元才可儲存 (the minimum number of bits required to store an unsigned 32-bit value without any leading zero bits),以下是一個可能的實作:

int ilog32(uint32_t v)
{
    int ret = v > 0;
    int m = (v > 0xFFFFU) << 4;
    v >>= m;
    ret |= m;
    m = (v > 0xFFU) << 3;
    v >>= m;
    ret |= m;
    m = (v > 0xFU) << 2;
    v >>= m;
    ret |= m;
    m = EXP10;
    v >>= m;
    ret |= m;
    EXP11;
    return ret;
}

請補完,使其行為符合預期。作答規範:

  1. EXP10EXP11 應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息
  2. EXP11 不該出現小括號 (即 ( 和 ))
  3. EXP10EXP11 皆包含 > 運算子

測驗 8

題目

考慮以下 C++ 二元搜尋樹的程式:

原本程式碼
typedef struct tnode *tree;                   
struct tnode {
    int data;   
    tnode *left;
    tnode *right;
    tnode(int d)
    {       
        data = d;
        left = right = 0;
    }           
};

void remove_data(tree &t, int d)
{
    tnode *p = t;
    tnode *q;
    while (p != 0 && p->data != d) {
        if (d < p->data) {
            q = p;
            p = p->left;
        } else {
            q = p;
            p = p->right;
        }
    }
    if (p != 0) {
        if (p->left == 0)
            if (p == t)
                t = p->right;
            else if (p == q->left)
                q->left = p->right;
            else
                q->right = p->right;
        else if (p->right == 0)
            if (p == t)
                t = p->left;
            else if (p == q->left)
                q->left = p->left;
            else
                q->right = p->left;
        else {
            tnode *r = p->right;
            while (r->left) {
                q = r;
                r = r->left;
            }
            p->data = r->data;
            if (r == p->right)
                p->right = r->right;
            else
                q->left = r->right;
            p = r;
        }
        delete p;
    }
}

函式 remove_data 嘗試將指定的資料,自給定的二元搜尋樹移除,但原本的 remove_data 過於冗長,於是我們可善用 indirect pointer 改寫為以下:

void remove_data(tree &t, int d)
{
    tnode **p = &t;
    while (*p != 0 && (*p)->data != d) {
        if (d < (*p)->data)
            EXP12;
        else
            EXP13;
    }
    tnode *q = *p;
    if (!q)
        return;

    if (!q->left)
        *p = q->right;
    else if (!q->right)
        *p = q->left;
    else {
        tnode **r = &q->right;
        while ((*r)->left)
            r = EXP14;
        q->data = (*r)->data;
        q = *r;
        *r = q->right;
    }
    delete q;
}

請補完,使其行為符合預期。作答規範:

EXP12, EXP13EXP14 應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息,並善用 indirect pointer 的技巧來撰寫
EXP12, EXP13EXP14 皆不該出現 ;

測驗 9

題目

考慮以下進行記憶體地址對齊 (alignment) 的巨集:

/* maximum alignment needed for any type on this platform, rounded up to a
   power of two */
#define MAX_ALIGNMENT 16

/* Given a size, round up to the next multiple of sizeof(void *) */
#define ROUND_UP_TO_ALIGNMENT_SIZE(x) \
    (((x) + MAX_ALIGNMENT - MMM) & ~(NNN))

作用是向上對齊 (roundup),請補完程式碼,使其行為符合預期。作答規範:

  • MMM 是常數
  • NNN 是表示式
  • MMMNNN 皆不得出現小括號 (即 ( 和 ))
  • 符合作業一的排版和程式碼風格規範
  • 以符合 C99 的最精簡形式撰寫

測驗 10

題目

考慮以下巨集可得到二個表示式進行整數除法操作時,最接近的整數值:

#define DIVIDE_ROUND_CLOSEST(x, divisor)                       \
    ({                                                         \
        typeof(x) __x = x;                                     \
        typeof(divisor) __d = divisor;                         \
        (((typeof(x)) -1) > 0 || ((typeof(divisor)) -1) > 0 || \
         (((__x) > 0) == ((__d) > 0)))                         \
            ? ((RRR) / (__d))                  \
            : ((SSS) / (__d));                 \
    })

注意: 當除數 (即 divisor) 為負值時,行為沒有定義。
參考執行結果:

  • DIVIDE_ROUND_CLOSEST(7, 3) = 2
  • DIVIDE_ROUND_CLOSEST(6, 3) = 2
  • DIVIDE_ROUND_CLOSEST(5, 3) = 2

作答規範:

  • 符合作業一的排版和程式碼風格規範
  • RRRSSS 為表示式,且都包含 (__x)(__d) (注意小括號)
  • RRRSSS 限用 +, -, >>, << 這幾個運算子,可搭配小括號,並以符合 C99 的最精簡形式撰寫
  • 變數在 RRRSSS 出現的順序 (從左到右),必定是 __x 先於 __d
  • 請補完程式碼,使其行為符合預期。

測驗 11

題目

考慮以下計算整數開平方根的實作:

static inline unsigned long fls(unsigned long word)
{
    int num = 64 - 1;
        
    if (!(word & (~0ul << 32))) {
        num -= 32;
        word <<= 32;
    }
    if (!(word & (~0ul << (64 - 16)))) {
        num -= 16;
        word <<= 16;
    }
    if (!(word & (~0ul << (64 - 8)))) {
        num -= 8;
        word <<= 8;
    }
    if (!(word & (~0ul << (64 - 4)))) {
        num -= 4;
        word <<= 4;
    }   
    if (!(word & (~0ul << (64 - 2)))) {
        num -= 2;
        word <<= 2;
    }   
    if (!(word & (~0ul << (64 - 1))))
        num -= 1;
    return num;
} 

unsigned long i_sqrt(unsigned long x)
{
    unsigned long b, m, y = 0;

    if (x <= 1)
        return x;

    m = 1UL << (fls(x) & ~1UL);
    while (m) {
        b = y + m;
        XXX;

        if (x >= b) {
            YYY;
            y += m;
        }
        ZZZ;
    }

    return y;
}

i_sqrt 函式的作用等同於 floor(sqrt(x))

作答規範:

  • 符合作業一的排版和程式碼風格規範
  • XXX, YYYZZZ 都是敘述,不得呼叫任何函式,且不包含 ; 字元
  • 只能使用 =, +=, -=, >>=, <<= 等運算子,且不得出現小括號 (即 ( 和 ))
  • 以符合 C99 的最精簡形式撰寫