Try   HackMD

第 10, 11, 12 週課堂問答簡記

Chiwawachiwawa

quiz4 紅黑樹

對照 hamt, hamt-bench

rbtree 的應用場景: mm, sched (CFS, $3.1)
rb_gen
設計實驗 ==> 檢驗 rbtree 的 locality
規模?
128 op -> 1024 op ->

chun61205

2023q1 第 11 週測驗題

barrier_cross 是怎麼執行的?

看到 barrier_cross

// test
/* before starting the test, we insert a number of elements in the data
 * structure.
 * we do this at each thread to avoid the situation where the entire data
 * structure resides in the same memory node.
 */
for (int i = 0; i < (int)d->n_add; ++i) {
    the_value =
        (val_t) my_random(&seeds[0], &seeds[1], &seeds[2]) & rand_max;
    /* we make sure the insert was effective (as opposed to just updating an
     * existing entry).
     */
    // if(push(the_list,the_value))
    if (list_insert(the_list, the_value) == 0)
        i--;
}

/* Wait on barrier */
barrier_cross(d->barrier);
void barrier_cross(barrier_t *b)
{
    pthread_mutex_lock(&b->mutex);
    b->crossing++;                /* One more thread through */
    if (b->crossing < b->count) { /* If not all here, wait */
        pthread_cond_wait(&b->complete, &b->mutex);
    } else {
        pthread_cond_broadcast(&b->complete);
        b->crossing = 0; /* Reset for next time */
    }
    pthread_mutex_unlock(&b->mutex);
}

看到 test 的說明,在實際測試時,會交錯進行插入和刪除,因此需要在測試之前進行測試前插入,以確保在刪除時 list 內部有節點可以刪除。
只要 thread 執行完測試前的插入,就會執行 barrier_cross ,並執行 b->crossing++ 代表多一個 thread ,直到 b->crossing < b->count 時,就會值行 p_thread_cond_broadcase 將其他 thread 叫起來,代表所有的 thread 都已經做完測試前插入。

  1. 這段程式碼的 condition variable 的使用很像 counting semaphore ,差別在於這裡有使用到 broadcase 發送信號給其他 thread 。
  2. 另外這個 barrier_cross 在 linux 有相對應的 API 實作,因此可以用 API 來改寫。

這裡我有個問題,就是為什麼在測試前插入時,會需要使用 barrier 來等待所有 thread 的測試前插入都執行完成?
仔細觀察程式碼可以發現,

barrier_init(&barrier, n_threads + 1);
void barrier_init(barrier_t *b, int n)
{
    pthread_cond_init(&b->complete, NULL);
    pthread_mutex_init(&b->mutex, NULL);
    b->count = n;
    b->crossing = 0;
}
void barrier_cross(barrier_t *b)
{
    pthread_mutex_lock(&b->mutex);
    b->crossing++;                /* One more thread through */
    if (b->crossing < b->count) { /* If not all here, wait */
        pthread_cond_wait(&b->complete, &b->mutex);
    } else {
        pthread_cond_broadcast(&b->complete);
        b->crossing = 0; /* Reset for next time */
    }
    pthread_mutex_unlock(&b->mutex);
}

barrier_init 會以 nthreads + 1 作為參數,並設定 b->count = n ,換句話說在 barrier_cross 的時候會等待 pthread_create 執行的數量再加上

1 個 thread 才會執行 pthread_cond_broadcast
繼續往下看到 main 可以發現

/* Start threads */
barrier_cross(&barrier);
gettimeofday(&start, NULL);
if (duration > 0) {
    /* sleep for the duration of the experiment */
    nanosleep(&timeout, NULL);
} else {
    sigemptyset(&block_set);
    sigsuspend(&block_set);
}

/* signal the threads to stop */
*running = 0;
gettimeofday(&end, NULL);

在所有 thread 都創建完後,才會再執行 barrier_cross 這時進入 barrier_cross 數量才會足夠,並把所有的 thread 叫起來開始測試,main 也會在這個時候開始計時。因此,使用 barrier 等待所有 thread 的測試前插入都執行完的原因是,為了能夠讓所有 thread 在同一個時間「起跑」。
然而,如此一來又會產生另外一個問題,因為這些 thread 事實上並不會同時執行,而是由 CPU scheduler 決定當前的資源要分給哪個 thread 執行。

Insert 和 Delete 的執行順序

if (op < read_thresh) { /* do a find operation */
    top(the_list);
if (list_insert(the_list, the_value)) {
    d->n_insert++;
    last = 1;
}
if (list_delete(the_list, the_value)) {
    d->n_remove++;
    last = -1;
}
d->n_ops++;

在插入一定的資料到鏈結串列後,會測試 insert 和 delete 以及「某個其他的動作」 (這裡是 top(the_list)) 的運作效能。
實際運作方式如下:

  1. 先進行一定數量的 top(the_list)
  2. list_insert
  3. list_delete
  4. 反覆進行步驟 3 和步驟 4

然而,這麼做會有一個問題,因為 insert 和 delete 是交錯進行,會造成能夠測試到的範圍固定的問題。
接下來,我希望能夠設計出一種方法,利用隨機數產生接下來執行的 operation 。
大略想法如下,

  1. 隨機數生成出的 operation 和目前鏈結串列中的資料數有關,資料越多,則生成出 delete 的機率越高。
  2. 因為當鏈結串列的資料數量為
    0
    時,不能執行 delete ,因此如果發生這樣的情況生成出 delete 的機率應該要為零。

設計這樣的方法有一個前題,也就是鏈結串列的容量固定。如果能夠事先估算鏈結串列的容量,則代表在接近容量滿的情況下,會再需要插入資料的可能性較低。

參考線性同餘方法 LCG

線性同餘方法- 维基百科,自由的百科全书

Nj+1=(A×NjB) mod M
其中

  1. Nj+1
    Nj
    分別是下個要產生出的隨機數和當前的隨機數。
  2. A
    ,
    B
    ,
    M
    為常數
    1. B
      ,
      M
      互質
    2. M
      的所有質因數能夠整除
      A1
    3. M
      4
      的倍數,則
      A1
      也是
    4. A
      ,
      B
      ,
      N0
      都小於
      M
    5. A
      ,
      B
      屬於正整數

事實上, glibc 的 rand 正是使用此方法來實作,其中,

  1. A=1103515245
  2. B=12345
  3. M=231

設計 operation 生成函式

barrier 的改寫

fennecJ

uintptr_t 是從什麼型態的衍生型態

為何需要使用 (uintptr_t) 轉型

try_flag 這個函式中,第 31 行的操作如下:

const uintptr_t new_val = (uintptr_t) n | F_FLAG;

為何需要在 n 前面加上 (uintptr_t),如果不加的話會有什麼問題?

會有 warning ?

好,我們把它拿掉直接編譯看看

...
error: invalid operands to binary | (have ‘struct nblist_node *’ and ‘int’)

這是因為 C 語言中的 bitwise 運算只能給整數型態使用,而 n 不是整數型態,所以會出錯。

list_search 的用途是找兩個連續的相鄰節點,這件事為何重要

我想是為了確保它 insert 時不會讓資料出錯?

你有看過 並行程式設計: Lock-Free Programming 這篇文章了嗎?

還沒。

沒關係,那我們現在來看:


現在有一個鏈結串列,我要把 10 移走,把 20 插進來,如果先發生 remove 才發生 insert 結果就會出錯:

所以找兩個相鄰節點目的就是找出可能發生這種錯誤的地方?

沒錯,在程式中我們會用 F_FLAG 和 F_MARK 來標出即將被移除的節點和已經被移除的節點。這裡有一件值得注意的事情,看到 list_delete 這個函式,你有在裡面看到任何釋放記憶體的程式碼嗎?

沒有。

對,這個函式真正做的事情只有更新該節點的 flag ,而真正移除節點的操作則是在

mimalloc

include/mimalloc/internal.h

Overflow detecting multiply

mmap(2)

If addr is NULL, then the kernel chooses the (page-aligned) address at which to create the mapping; this is the most portable method of creating a new mapping.

Korin777

struct page_header {
    size_t pages;
    size_t page_size;
    char page_data[];  // the rest of the page(s) of memory
};
struct page_header {
    size_t pages;
    size_t page_size;
    char *page_data;  // the rest of the page(s) of memory
};

為何將 struct page_headerpage_data 改為 char *page_data 會引發錯誤 => a.out: test.c:138: test_alloc_small: Assertion result == TEST_SUCCESS' failed.

fennecJ
是不是和 Flexible array members 有關

simple_malloc 我們要回傳記憶體地址給使用者,若將 page_data[] 改為 *page_data,此時 result->page_data 會是記憶體地址中的值

  • page_datapage_data[] 此時 result->page_data 相當於 &result->page_data[0]
  • page_data*page_data 此時 result->page_data 相當於 result->page_data[0]

因此把 page_data[] 改為 *page_data 就必須將所有 result->page_data 改為 &result->page_data 才會是我們要的記憶體地址,就能避免上述錯誤,這時會發現在 free 產生錯誤
sizeof 去看上面兩個結構體所佔的空間,可以發現 page_data[] 空間並沒有被算進去,所以 void_to_page_header 可以直接減去結構體大小來獲取 page_header , 也因此當我們將 page_data[] 改為 *page_data , 就會額外去扣掉 *page_data 所佔的空間,可以在 simple_free_internal 補回多扣掉的空間來解決這個問題

YSRossi

alloc.c 目前的 alloc 實作能否在多執行緒的環境運作?

不行,因為沒有保護

哪些部份需要被保護?

第 15 行之前都是區域變數,不需要保護。mmapMT-Safe,因此也不需要保護。應該保護的地方為第 23~24 行,也就是更新 page header 資訊的地方。

static struct page_header *alloc_internal(size_t size) { if (!size) return NULL; /* size in bytes of a page of memory */ size_t page_size = get_page_size(); /* size in bytes that needs to be allocated */ size_t size_with_header = size + sizeof(AAAA); /* number of pages to allocate */ /* TODO: fix case where size_with_header == page_size */ size_t pages = size_with_header / page_size + BBBB; size_t mmap_size = pages * page_size; void *ptr = mmap(0, mmap_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (ptr == MAP_FAILED) return NULL; struct page_header *page = ptr; /* copy values to page header */ page->pages = pages; page->page_size = page_size; return page; }

我仔細思考這一題,我覺得這題不必特別去保護資料就可以在多執行緒運作。

reallocfree 以功能來說,在傳入指標相同的狀況下一定不可能在二個以上執行緒運作。因為一旦一個執行緒為先進行 free ,另一個執行緒在傳入相同指標下執行 free 就變成 double-free , realloc 重新配置空間可能會改變指標,故一個執行緒成功,另一個執行緒就會對無效的指標進行 realloc 。故考慮多執行緒的狀況下,應只需要考慮 reallocfree 在不同指標的情況下同時執行的狀況。

老師在檢討時提到的 alloc_internal 函式來說,在多執行緒同時執行時,mmap 同時被執行,但回傳的指標應會不同。故下面的幾行指派 struct page_header 裡的 pagespage_size 部份應不會造成問題,不需要保護。

不過老師有提到,若加入 free list 來充份利用空間,而不是每此配置只能以 page 為單位來配置空間,維護 free list 就需要特別保護。

yanjiew1

如果為了使用較小的空間而配置一個 page,這樣很浪費資源,我們可以配置一個 page 重複使用這個空間,那你會用什麼較不複雜的資料結構來管理?

雖然使用單向鏈結串列較省空間,但通常會使用雙向 linked list,相較於單向鏈結串列有較快的搜尋速度。

我們需要搜尋 free list 尋找符合請求的空間,所以搜尋複雜度為一個考量點。選擇使用 red-black tree 來管理 free list ,雖然搜尋速度不是最快的,重點是 worst case 的時間複雜度為 O(log n)。

concurrent hashtable

  • concurrent hashtable
  • deep copy
  • 運用 callback 處理數值傳遞和資源釋放
  • 呼叫 sched_yield

ht_get_list 的作用

本次程式碼使用 seperate chaining 的機制 (示意圖如下)以處理 hash table 中發生 collision 的問題, 其中 ht_get_list 的作用如其名稱,將回傳作為 bucket 所使用的 linked list。

deep copy 的存在必要是在並行的環境中,避免 dangling pointer

Copy 主要可以分為 shallow copy 和與其相對的 deep copy,其中 shallow copy 並不完全 "複製" 出一個物件,而是複製出一個物件的參照 (如: 指標)。 例如 python 中的 copy 作法是創造一個物件並將原物件內含的物件裝入,示意圖如下。

  • A shallow copy constructs a new compound object and then (to the
    extent possible) inserts the same objects into it that the
    original contains.

而 deep copy 相對於 shallow copy,除了複製物件本身之外也會一併複製內含的物件,新物件和就物件可以看成相同屬性的量個獨立個體,示意圖如下。 由於內容物也需要配置記憶體,所消耗資源也比 shallow copy 多上許多。

  • A deep copy constructs a new compound object and then, recursively,
    inserts copies into it of the objects found in the original.

在 cocurrent 的需求下,物件何時會被哪個執行序進行修改是無法得知的,使用 deep copy 可以避免其他執行序若持有該物件的參照進而在執行過程中產生 dangling pointer。

iterator 在走訪個別元素時,可能會有其他執行緒變更