Try   HackMD

2025q1 Homework1 (lab0)

contributed by <fcu-D0812998>

作業書寫規範:

  • 無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
  • 文字訊息 (尤其是程式執行結果) 請避免用圖片來表示,否則不好搜尋和分類
  • 共筆書寫請考慮到日後協作,避免過多的個人色彩,用詞儘量中性
  • 不要在筆記內加入 [TOC] : 筆記左上方已有 Table of Contents (TOC) 功能,不需要畫蛇添足
  • 不要變更預設的 CSS 也不要加入任何佈景主題: 這是「開發紀錄」,用於評分和接受同儕的檢閱
  • 在筆記中貼入程式碼時,避免非必要的行號,也就是該手動將 c=cpp= 變更為 ccpp。行號只在後續討論明確需要行號時,才要出現,否則維持精簡的展現。可留意「你所不知道的 C 語言: linked list 和非連續記憶體」裡頭程式碼展現的方式
  • HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」
  • 留意科技詞彙的使用,請參見「資訊科技詞彙翻譯」及「詞彙對照表
  • 不要濫用 :::info, :::success, :::warning 等標示,儘量用清晰的文字書寫。:::danger 則僅限授課教師作為批注使用
  • 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景
  • 在中文敘述中,使用全形標點符號,例如該用「,」,而非 ","。注意書名號的使用,即 ,非「小於」和「大於」符號
  • 避免使用不必要的 emoji 字元

開發環境

開發與實驗環境

​​​​~$ gcc --version
​​​​gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

​​​​~$lscpu
​​​​架構:                    x86_64
​​​​  CPU 作業模式:          32-bit, 64-bit
​​​​  Address sizes:          39 bits physical, 48 bits virtual
​​​​  Byte Order:             Little Endian
​​​​CPU(s):                   6
​​​​  On-line CPU(s) list:    0-5
​​​​供應商識別號:            GenuineIntel
​​​​    Model name:             Intel(R) Core(TM) i5-9500 CPU @ 3.00GHz
​​​​    CPU 家族:            6
​​​​    型號:                158
​​​​    每核心執行緒數:      1
​​​​    每通訊端核心數:      6
​​​​    Socket(s):            1
​​​​    製程:                10
​​​​    CPU(s) scaling MHz:   61%
​​​​    CPU max MHz:          4400.0000
​​​​    CPU min MHz:          800.0000
​​​​    BogoMIPS:             6000.00

queue 實作結果與過程

q_new

Create an empty queue whose next and prev pointer point to itself
Return: NULL for allocation failed

struct list_head *q_new()
{
    struct list_head *new = malloc(sizeof(struct list_head));
    if (!new)
        return NULL;
    INIT_LIST_HEAD(new);
    return new;      
}

使用 malloc 動態分配記憶體來建立一個 struct list_head 節點,並初始化其 next 和 prev 指向自身,形成一個空佇列。
去規格書參考有關malloc相關的資料,看到

  • 7.20.3 Memory management functions
    If the space cannot be allocated, a null pointer is returned.

這跟函式規則Return: NULL for allocation failed剛好一致,所以無需判斷。

q_free

Free all storage used by queue, no effect if header is NULL

先判斷queue中有無節點,有節點的話利用list_for_each_entry_safe依序刪除往後的節點,迴圈出來後再free head的空間。

void q_free(struct list_head *head)
{
    if (!head) {
        return;
    }
    element_t *entry = NULL, *safe;
    list_for_each_entry_safe (entry, safe, head, list) {
        list_del(&entry->list);
        q_release_element(entry);
    }
    free(head);
}

q_insert_head

Insert an element in the head.
Argument s points to the string to be stored.
The function must explicitly allocate space and copy the string into it.
Return: true for success, false for allocation failed or queue is NULL

bool q_insert_head(struct list_head *head, char *s)
{
    if (!head)
        return false;
    element_t *new = malloc(sizeof(element_t));
    if (!new)
        return false;
    int len = strlen(s) + 1;
    new->value = malloc(len);
    if (!(new->value)) {
        free(new);
        return false;
    }
    memcpy(new->value, s, len);
    list_add(&new->list, head);
    return true;
}

先利用 strlen(s) 取得s的字串長度,再利用 malloc 分配出等成的空間給字串s,再把s的資料複製到new中,再利用 list_add 插在queue的最前端。
然後根據 C99 規格書中有關 string length 的敘述。

  • C99 7.1.1 Definitions of terms

    The length of a string is the number of bytes preceding the null character

根據規格書中的說明,在C語言當中,字串的長度不包含結尾的 \0。
再去看看規格書中說明strlen的部分。

  • C99 7.21.6.3 The strlen function

    The strlen function returns the number of characters that precede the terminating null character

這段說明strlen 返回的是 null 字符 (\0) 之前的字元數量,所以如果我們需要正確儲存一個C語言字串,應該分配 strlen(s) + 1,確保有額外空間存放 \0
接下來要將字串複製到new中,根據規格書中7.21.2.4章節說明如果 src 的前 n 個字元中沒有 \0,則結果不會有null終止符,這樣 new_entry->value 可能變成未終止的字串,在後續使用時可能導致未定義行為,因此選擇使用memcpy

  • C99 7.21.2.4 The strncpy function

    The strncpy function copies not more than n characters (characters that followanull character are not copied) from the array pointed to by s2 to the array pointed to by s1. If copying takes place between objects that overlap, the behavior is undefined.

最後再利用list_add把新的 new 加入到 list 中。

q_insert_tail

Attempt to remove element from tail of queue.
Other attribute is as same as q_remove_head.

bool q_insert_tail(struct list_head *head, char *s)
{
    if (!head)
        return false;
    element_t *new = malloc(sizeof(element_t));
    if (!new)
        return false;
    size_t len = strlen(s) + 1;
    new->value = malloc(len);
    if (!(new->value)) {
        free(new);
        return false;
    }
    memcpy(new->value, s, len);
    list_add_tail(&new->list, head);
    return true;
}

這邊想法跟q_insert_head一樣,差在最後要使list_add_tail確保值插入在尾端

q_remove_head

Attempt to remove element from head of queue.
Return target element.
Return NULL if queue is NULL or empty.
If sp is non-NULL and an element is removed, copy the removed string to *sp
(up to a maximum of bufsize-1 characters, plus a null terminator.)
NOTE: "remove" is different from "delete"
The space used by the list element and the string should not be freed.
The only thing "remove" need to do is unlink it.

element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
    if (!head || list_empty(head))
        return NULL;
    element_t *remove_node = list_first_entry(head, element_t, list);
    list_del(&remove_node->list);
    if (sp) {
        memcpy(sp, remove_node->value, bufsize - 1);
        sp[bufsize - 1] = '\0';
    }
    return remove_node;
}

利用list_first_entry先取得queue的第一個element的地址且因為是remove所以要將移除點的直放進sp中且最多只複製bufsize - 1的字元,因為超過就無法補\0,且最後也需要手補\0來表示結束。

q_remove_tail

Attempt to remove element from tail of queue.
Other attribute is as same as q_remove_head.

element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
    if (!head || list_empty(head))
        return NULL;
    element_t *remove_node = list_last_entry(head, element_t, list);
    list_del(&remove_node->list);
    if (sp) {
        memcpy(sp, remove_node->value, bufsize - 1);
        sp[bufsize - 1] = '\0';
    }
    return remove_node;
}

q_remove_head一樣,除了前面一開始要使用list_last_entry

q_size

Return number of elements in queue.
Return 0 if q is NULL or empty

int q_size(struct list_head *head)
{
    if (!head || list_empty(head))
        return 0;
    struct list_head *node;
    int size = 0;
    list_for_each (node, head)
        size++;
    return size;
}

list_for_each會保證完整遍歷鏈結串列,size 就是鏈結串列內的節點數。

q_delete_mid

Delete the middle node in list.
The middle node of a linked list of size n is the
⌊n / 2⌋th node from the start using 0-based indexing.
If there're six element, the third member should be return.
Return true if successful.
Return false if list is NULL or empty.

bool q_delete_mid(struct list_head *head)
{
    // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
    if (!head || list_empty(head))
        return 0;
    struct list_head *slow = head->next, *fast = slow->next;
    while (fast != head && fast->next != head) {
        slow = slow->next;
        fast = fast->next->next;
    }
    if (fast != head)
        slow = slow->next;
    list_del(slow);
    q_release_element(list_entry(slow, element_t, list));
    return true;
}

使用快慢指標找出中間的位置再將此節點刪除。

q_delete_dup

Delete all nodes that have duplicate string,
leaving only distinct strings from the original list.
Return true if successful.
Return false if list is NULL.

bool q_delete_dup(struct list_head *head)
{
    // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
    if (!head || list_empty(head))
        return false;
    element_t *entry = NULL, *safe;
    bool dup = false;
    list_for_each_entry_safe (entry, safe, head, list) {
        if (entry->list.next != head &&
            strcmp(entry->value,
                   list_entry(entry->list.next, element_t, list)->value) == 0) {
            dup = true;
            list_del(&entry->list);
            q_release_element(entry);
        } else if (dup) {
            list_del(&entry->list);
            q_release_element(entry);
            dup = false;
        }
    }
    return true;
}

因為要刪除節點所以使用list_for_each_entry_safe,利用strcmp判斷entry和下一個節點的 value是否相同,直到判斷到不同,再將entry的list全部刪除掉,此外這邊在尚未將entry初始直射成NULL之前出現了這樣的錯誤

q_swap

Attempt to swap every two adjacent nodes.

void q_swap(struct list_head *head)
{
    // https://leetcode.com/problems/swap-nodes-in-pairs/
    if (!head || list_empty(head))
        return;
    struct list_head *node = head->next;
    while (node != head && node->next != head) {
        struct list_head *node2 = node->next;
        list_del(node);
        list_add(node, node2);
        node = node->next;
    }
}

想法是先設node跟node2為頭兩個節點,先斷開第一個節點,再將node2插入到node前面,這樣就完成了 相鄰節點的交換。

q_reverse

Reverse elements in queue
No effect if q is NULL or empty
This function should not allocate or free any list elements
(e.g., by calling q_insert_head, q_insert_tail, or q_remove_head).
It should rearrange the existing ones.

void q_reverse(struct list_head *head)
{
    if (!head || list_empty(head))
        return;
    struct list_head *pos, *safe;
    list_for_each_safe (pos, safe, head)
        list_move(pos, head);
}

使用list_move從第一個節點開始將每個節點插入在head後面,這樣就完成反轉。

q_reverseK

Given the head of a linked list, reverse the nodes of the list k at a time.
No effect if queue is NULL or empty. If there has only one element, do nothing.

void q_reverseK(struct list_head *head, int k)
{
    // https://leetcode.com/problems/reverse-nodes-in-k-group/
    if (!head || list_empty(head))
        return;
    struct list_head *pos, *safe, *cut = head;
    int count = 0;
    list_for_each_safe (pos, safe, head) {
        count++;
        if (count % k == 0) {
            LIST_HEAD(tmp);
            count = 0;
            list_cut_position(&tmp, cut, pos);
            q_reverse(&tmp);
            list_splice(&tmp, cut);
            cut = safe->prev;
        }
    }
}

利用count做記數,當count % k == 0的話就能確保k個一組做反轉,先用list_cut_position把要反轉的節點剪切出來,存入 tmp,再利用剛剛q_reverse做反轉,再用list_splice把它存回去。

q_ascend && q_descend

Remove every node which has a node with a strictly less value anywhere to the right side of it.
Remove every node which has a node with a strictly greater value anywhere to the right side of it.
No effect if queue is NULL or empty. If there has only one element, do nothing.Memory allocated to removed nodes must be freed.

int q_ascend(struct list_head *head)
{
    // https://leetcode.com/problems/remove-nodes-from-linked-list/
    if (!head || list_empty(head))
        return 0;
    element_t *right = list_entry(head->prev, element_t, list);
    element_t *left = list_entry(head->prev->prev, element_t, list);
    while (&left->list != head) {
        if (strcmp(right->value, left->value) < 0) {
            list_del(&left->list);
            element_t *tmp = left;
            left = list_entry(left->list.prev, element_t, list);
            free(tmp->value);
            free(tmp);

        } else {
            left = list_entry(left->list.prev, element_t, list);
            right = list_entry(right->list.prev, element_t, list);
        }
    }
    return q_size(head);
}

利用strcmp比較最後兩個節點的大小,若left大於right的話,就用list_del() 從 queue 中移除 left,但不會釋放記憶體,然後再用tmp 暫存 left 指向的節點,這樣我們可以稍後 free(tmp),再將left往前指到前一個節點。

int q_descend(struct list_head *head)
{
    // https://leetcode.com/problems/remove-nodes-from-linked-list/
    if (!head || list_empty(head) || list_is_singular(head))
        return 0;
    element_t *right = list_entry(head->prev, element_t, list);
    element_t *left = list_entry(head->prev->prev, element_t, list);
    while (&left->list != head) {
        if (strcmp(right->value, left->value) > 0) {
            list_del(&left->list);
            free(left->value);
            free(left);
            left = list_entry(right->list.prev, element_t, list);
        } else {
            left = list_entry(left->list.prev, element_t, list);
            right = list_entry(right->list.prev, element_t, list);
        }
    }
    return q_size(head);
}

descend 一樣的原理只是將strcmp中改成大於。

q_sort

Sort elements of queue in ascending/descending order.
No effect if queue is NULL or empty. If there has only one element, do nothing.

void _list_merge(struct list_head *left, struct list_head *right, bool descend)
{
    struct list_head head;
    INIT_LIST_HEAD(&head);
    while (!list_empty(left) && !list_empty(right)) {
        int cmp = strcmp(list_first_entry(left, element_t, list)->value,
                         list_first_entry(right, element_t, list)->value);
        if ((descend && cmp > 0) || (!descend && cmp <= 0)) {
            list_move_tail(left->next, &head);
        } else {
            list_move_tail(right->next, &head);
        }
    }
    if (!list_empty(right))
        list_splice_tail(right, &head);
    list_splice(&head, left);
}
void q_sort(struct list_head *head, bool descend)
{
    if (!head || list_empty(head) || list_is_singular(head))
        return;
    struct list_head *mid = head;
    int size = q_size(head) / 2;
    for (int i = 0; i < size; i++)
        mid = mid->next;
    struct list_head left, right;
    INIT_LIST_HEAD(&left);
    INIT_LIST_HEAD(&right);
    list_cut_position(&left, head, mid);
    list_cut_position(&right, head, head->prev);
    q_sort(&left, descend);
    q_sort(&right, descend);
    _list_merge(&left, &right, descend);
    list_splice(&left, head);
}

參考了老師放在作業要求上的Linked List Sort知道了merge_sort的複雜度為 O(nlogn)為所有排序法中複雜度最低的,所以想法是實作merge sort,首先先用list_cut_position且在用遞迴的方式將queue切成一個一個,再另外寫一個函式 _list_merge 主要是判斷傳入的兩個字串比較大小,再用把list_splice 把head 內的排序結果合併到 left,讓 left 變成合併後的完整 queue。

q_merge

int q_merge(struct list_head *head, bool descend)
{
    // https://leetcode.com/problems/merge-k-sorted-lists/
    if (!head || list_empty(head))
        return 0;
    else if (list_is_singular(head))
        return q_size(list_first_entry(head, queue_contex_t, chain)->q);
    queue_contex_t *tmp = NULL,
                   *first = list_first_entry(head, queue_contex_t, chain);
    list_for_each_entry (tmp, head, chain) {
        if (tmp && tmp->q && tmp != first) {
            list_splice_tail_init(tmp->q, first->q);
            first->size += tmp->size;
            tmp->size = 0;
        }
    }
    q_sort(first->q, descend);
    return first->size;
}

想法是將要merge 的 queue 全都塞進一個queue 裡,再呼叫剛剛寫好的q_sort 去做排序。

qtest 提供新的命令 shuffle

看了老師在作業放的Fisher–Yates shuffle 演算法

-- To shuffle an array a of n elements (indices 0..n-1):
for i from 0 to n−2 do
     j ← random integer such that i ≤ j ≤ n-1
     exchange a[i] and a[j] 
bool q_shuffle(struct list_head *head)
{
    if (!head || list_empty(head))
        return false;
    srand(time(NULL));
    struct list_head *cur = head->next, *tail = head->prev;
    for (int i = q_size(head); i > 0; i--) {
        int rd_num = rand() % (i);
        for (int j = 0; j < rd_num; j++) {
            cur = cur->next;
        }
        struct list_head *tmp = cur->next;
        list_move_tail(cur, tail);
        tail = tail->prev;
        cur = tmp;
    }
    return true;
}

想法很簡單,就先用兩個指標指到queue的頭尾,頭代表每次取到隨機數字就可以從頭開始往下指,尾的部分就是交換後完再將尾的部分往前指一個,並在qtest.c加入shuffle命令。

ADD_COMMAND(shuffle,
            "Shuffle the queue with Fisher–Yates shuffle algorithm", "");

並在qtest.c新增do_shuffle()測試

static bool do_shuffle(int argc, char *argv[])
{
    if (argc != 1) {
        report(1, "%s takes no arguments", argv[0]);
        return false;
    }
    if (!current) {
        report(3, "Warning: Try to operate null queue");
        return false;
    }
    if (exception_setup(true))
        q_shuffle(current->q);
    exception_cancel();

    q_show(0);
    return !error_check();
}

後來還是測不過,發生錯誤

[!] You are not allowed to change the header file queue.h or list.h

詢問同學h0w726,才知道需要加入標頭檔。

bool q_shuffle(struct list_head *head);

統計方法驗證 shuffle

  1. 首先先計算 chi-squared test statistic

x2=i=0n(OiEi)2Ei

後來先將 test_count 降到1000次先觀察每種結果各自的頻率,發現只有某四種結果有出現過,後來發現是 srand(time(NULL)) 的問題,是因為當你在極短時間內重複呼叫到srand(time(NULL)),那麼 time(NULL)傳回的值是一樣的(精度只有秒),你等於執行了多次 srand(相同的值),導致後續rand()產生的亂數序列也一樣。
在測試 shuffle 1000000 次後,每種結果各自的頻率如下表:

觀察到的頻率 期待的頻率
(OiEi)2Ei
[1, 2, 3, 4] 41526 41666 0.47040752652042434
[1, 2, 4, 3] 41810 41666 0.497671962751404
[1, 3, 2, 4] 41780 41666 0.3119089905438487
[1, 4, 2, 3] 41522 41666 0.497671962751404
[1, 4, 3, 2] 42020 41666 3.0076321221139537
[2, 1, 3, 4] 41352 41666 2.3663418614697833
[2, 1, 4, 3] 41724 41666 0.08073729179666875
[2, 3, 1, 4] 41519 41666 0.5186242979887679
[2, 3, 4, 1] 41435 41666 1.2806844909518553
[2, 4, 1, 3] 41776 41666 0.2904046464743436
[2, 4, 3, 1] 41520 41666 0.5115921854749677
[3, 1, 2, 4] 41725 41666 0.08354533672538761
[3, 1, 4, 2] 41351 41666 2.381438103009648
[3, 2, 1, 4] 41727 41666 0.08930542888686219
[3, 2, 4, 1] 41968 41666 2.1889310228963663
[3, 4, 1, 2] 41685 41666 0.00866413862621802
[3, 4, 2, 1] 41603 41666 0.09525752412038592
[4, 1, 2, 3] 41779 41666 0.306460903374454
[4, 1, 3, 2] 41227 41666 4.625378006048097
[4, 2, 1, 3] 41592 41666 0.13142610281764508
[4, 2, 3, 1] 41920 41666 1.5484087745403927
[4, 3, 1, 2] 41957 41666 2.0323765180242885
[4, 3, 2, 1] 41604 41666 0.09225747611961792
Total 23.417126674026782
  1. 決定自由度(degrees of freedom)
  • suffle 4個數字所以有24種結果,所以自由度為23
  1. 選擇顯著水準
  • alpha 設為常見的0.05
  • 從卡方分布表找合適的 P value,自由度為23,
    X2
    為 23.417126674026782,因為 22.037 < 23.91283060528968 < 26.018,於是 P value 介於 0.5 和 0.3 之間。
  1. 統計檢定的結果不拒絕虛無假說
  • 因為 P value(0.3~0.5)> alpha(0.05),統計檢定的結果不拒絕虛無假說(
    H0
    )
  • 從圖表來看 shuffle 的頻率是大致符合 Uniform distribution 的image

研讀論文〈Dude, is my code constant time?〉

Side-channel attack 是一種不是針對密碼學演算法本身的破解方式,而是利用程式或硬體實作中非預期的特徵,來側面推測機密資訊。非預期的特徵就像是 Timing Attack,它會透過觀察程式執行所花的時間差異,來分析輸入資料是否接近正確。

比方說,某段程式會一個字元一個字元地比對密碼,只要遇到不符就立即結束並回傳錯誤。
攻擊者可以藉由大量嘗試,觀察執行時間的長短,就可能一步步推敲出正確密碼的內容。
為了對抗這種攻擊,現在已經有一些工具可以用來檢測程式是否有時間上的漏洞,像是

  • ctgrind:是 Valgrind 的延伸工具,透過動態追蹤程式的行為。
  • ctverif:則是利用形式化驗證進行靜態分析。

但這些工具有個共通限制,它們需要對底層硬體(如 CPU)做詳細建模。而這在實務上很困難,因為晶片製造商往往不會公開這些內部細節。

為了解決這個方法這篇論文就開發了一套叫做 dudect 的工具,它不透過靜態分析,而是直接執行程式,並大量蒐集執行時間,再用統計方法分析差異。

這種方式稱作 leakage detection test,它只需要在目標平台上執行就可以使用,完全不需要了解或模擬硬體的細節。因此 dudect 特別適合用來驗證程式在實際環境下是否為 constant-time,不受限於特定硬體或理論模型。

方法部分主要分成三種

  • Step 1: Measure execution time
    測試時會使用兩組不同特性的輸入資料,在同樣的環境下計算他們兩者的時間分佈,分別
    一組是固定輸入,另一組是隨機輸入
  • Step 2: Apply post-processing
    a) Cropping : dudect 是靠收集執行時間來做統計分析的,但實際在跑測試時,這些時間數據中可能會混入 Noise ,像是測量過程被 OS 中斷、背景程式干擾,實際上 dudect 在實作上會刪除過於極端的時間
    b) Higher-order preprocessing 統計測試可以透過 high-order preprocessing 得到更有效的分析結果,因為在有些情況下,洩漏時間不一定在平均值能發現。
  • Step 3: Apply statistical test
    dudect 會將之前收集到的執行時間分為兩組,使用統計檢定來驗證「這兩組資料是否來自同一個分布」。主要採用 Welch’s t-test,來判斷兩組平均值是否顯著不同。如果有差異,就代表該程式可能會洩漏 timing 資訊,不符合 constant-time 的安全要求。

解釋 Student's t-distribution

當我們在進行統計檢定時,通常會用常態分布來估計資料的行為,但這只有在樣本數夠多的情況下才適用。當樣本數較小時,資料本身的變異性較大,這時使用常態分布可能會低估極端值出現的機率,導致檢定結果不夠保守。為了解決這個問題,就會使用 Student’s t-distribution(學生 t 分布)。t 分布與常態分布長得類似,都是中間高、兩側低的鐘形曲線,不同的是 t 分布在自由度較小時,兩側尾巴會更厚,表示更高的極端值機率,能反映小樣本時的不確定性。隨著樣本數增加,t 分布會漸漸趨近標準常態分布。這種分布通常會用在像 t-test 這類的假設檢定中,特別是當我們要比較兩組平均值是否顯著不同,而又不知道母體標準差時。在 dudect 這類工具中,Student’s t-distribution 被用來統計分析「固定輸入」與「隨機輸入」在執行時間上的差異,進而判斷一段程式是否具備 constant-time 性質,避免時間洩漏發生。
image

解釋本程式的 "simulation" 模式

在 lab0-c 中,simulation 模式被設計來測試 queue 函式的時間複雜度是否為constant-time。就分成剛剛上面提及的三步驟來做分析

  • Step 1: Measure execution time
    首先要計算兩組不同特性的輸入資料的時間差,在 /dudect/fixture.c 可以看到
exec_times[i] = after_ticks[i] - before_ticks[i];

這段程式會把每次操作的「結束時間 - 開始時間」算出來,記錄成每一次操作的實際執行時間,再來丟進下面一點的程式碼中

int64_t difference = exec_times[i];
t_push(t, difference, classes[i]);

把所有執行時間資料根據類別(固定or隨機)分組後,餵進 t-test 所需的統計計算中

  • Step 2: Apply post-processing(在 lab0-c 實作 percentile )
    因為 lab0-c 沒有具備 percentile 的處理,所以參考了老師給的oreparaz/dudect,主要核心在這段程式碼
/*
 set different thresholds for cropping measurements.
 the exponential tendency is meant to approximately match
 the measurements distribution, but there's not more science
 than that.
*/
static void prepare_percentiles(dudect_ctx_t *ctx) {
  qsort(ctx->exec_times, ctx->config->number_measurements, sizeof(int64_t), (int (*)(const void *, const void *))cmp);
  for (size_t i = 0; i < DUDECT_NUMBER_PERCENTILES; i++) {
    ctx->percentiles[i] = percentile(
        ctx->exec_times, 1 - (pow(0.5, 10 * (double)(i + 1) / DUDECT_NUMBER_PERCENTILES)),
        ctx->config->number_measurements);
  }
}

先利用 q_sort 將執行時間排序從小到大,會利用 cmp 去比較執行時間大小

static int cmp(const int64_t *a, const int64_t *b) {
    if (*a == *b)
        return 0;
    return (*a > *b) ? 1 : -1;
}

percentile 則是利用來判斷在這個百分比下對應到的是排序好的執行時間陣列裡的哪一個地方

static int64_t percentile(int64_t *a_sorted, double which, size_t size) {
    size_t array_position = (size_t)((double)size * (double)which);
    assert(array_position < size);
    return a_sorted[array_position];

所以這邊就要來在 lab0 中實作 percentile 的功能,先把 cmp 跟 percentile 這兩個 function 放入 lab0-c/dudect/ttest.c 以方便 fixture.c 呼叫。
而在 fixture.c 中在 doit 中放入 prepare_percentiles 函式讓他能避免掉極端值影響。

  • Step 3: Apply statistical test
    最後在 t_compute() 中利用公式

    t=x¯0x¯1s02n0+s12n1

    分子為 兩組輸入的執行時間平均值
    分母為 「標準誤差(standard error)」,是衡量兩組平均值的差異有多可靠的依據
    而根據論文中提及

​A t value larger than 4.5 provides strong statistical evidence that the distributions 
​are different.                             

當 t 值絕對值大於 4.5,表示兩個輸入類別的執行時間分布統計上有顯著差異