contributed by < kevinshieh0225
>
1
: ceil log_2(x)以下程式碼實現對 x 取 ,我們先依序為程式碼下十進位理解下的註解:
大約歸納程式碼後說明流程:
x >> 1
EXP1
= r | shift | x >> 1
參考 jim12312321
,發現這個函式是 binary search!我們用改成用二進位視角來理解:
在數學定義上 是未定義行為,而如果以上面的函式帶入 x = 0,會導致 x--
溢位而使輸出結果變成 33。
如果我們 x--
的目的是為了處理上界的不一致,那其實也可以在最後對輸出做調整:
在我們開始對 x 作位移之前先計算 int cps = (x & (x-1)) != 0;
,用以判斷此數是否為 的數值,如果是的話我們就不在最後做加一的補償。這除了解決了數值對應範圍不一致的問題,也處理了 x = 0 的例外狀況。此時我們帶入 0 的結果是 0,這樣的結果對我來說是相對可以接受的。
實作詳見 ceil_log2.c
2
: find first set根據 Find first set 的定義,此函式回傳 least significant 1-bit 的位置。方法本身如同上一題 find last set 的手法,利用 binary search 尋值:
EXP2
: x & t
EXP3
: o += shift
在第一題由於我們輸入參數型態為 unsigned int32_t
所以數值長度必定為 32 位元,但本題 unsigned long
會因為不同的資料模型而有 32 或 64 的差異,我們利用條件式來判斷第一次(32 bit)的位移是否執行,接著使用第一題的技巧實作一個幾乎 branchless 的函式:
實作詳見 ffs.c
linux/include/asm-generic/bitops/
根據不同的使用情境,ffs
有了不同的定義,好比說是否能用 gnu built_in、輸入型態 int
, long
…。
在其中有一份 __ffs.h
與本題實作情境相同:
程式碼邏輯相同,不過他沒有定義 word
為 0 時的特例。
3
: foo_consumerconsumer_del
函式要從串鏈 *foo
中刪除 *fc
節點,使用 indirect pointer 找到該節點後移除節點,並最後回傳執行是否成功。
使用 foo_consumer
的好處就是透過將功能抽離,達到模組化,可提升程式的重複利用性、可維護性。
EXP4
: con = &(*con)->next
EXP5
: fc->next
4
: Coroutine以下嘗試用 lab0 提及的 setjmp 和 longjmp,用以實作〈你所不知道的 C 語言: goto 和流程控制篇〉闡述的 coroutine,參考的程式執行輸出如下:
setjmp
與 longjmp
函數可以讓程式往回跳到函數呼叫堆疊中的某個函數中,就像是一種跨函數的 goto
。
根據文件:
在我們達成跨函式 goto
的前提,我們使用 jmp_buf
來記錄此刻在這個函式執行的資訊。setjmp
紀錄此刻的 jmp_buf
,並提供跳躍點。我們透過 setjmp
的回傳值來確認目前是在做紀錄(return 0),還是我們從其他函式跳躍回來到目前指令(return non-zero)。
用 longjmp
來執行跳躍,引數 env
指出我們將跳躍的 env 指令位置?val
是設定我們跳躍到該 setjmp
位置時的函式回傳值。
在 coroutine
的實作中,我們需要頻繁在任務函式間跳躍,如何管理跳躍資訊 jmp_buf
是一大問題。本次實作我們利用雙向佇列 tasklist
來紀錄並管理我們跳躍的排程。task_add
函式將 jmp_buf
安插在佇列的末尾,供未來排程排序。task_switch
與 task_join
將從佇列選擇首節點進行指令跳躍,也同時刪除節點取消追蹤。
schedule
規劃 coroutine
的執行,他規劃每個 task 應該要執行幾次、確認 task 是否完成所有任務。
首先 schedule
確認是否所有的任務都被指派任務次數了,如果尚未指派完就會進到 while 迴圈內,提供一個小於 5 的正整數 n
執行 tasks[i++](&n);
的函式。printf("Never reached\n");
這一條不會被執行,因為每個 task 在執行完時會跳躍到 setjmp(sched);
。
在確認任務都被指派任務次數後,每次跳躍到setjmp(sched);
後,會直接略過 while 迴圈進到 task_join(&tasklist);
檢查任務是否執行完畢。
最後來理解一下 task
在做的事情:task
初始化後,設立跳躍點 setjmp(env)
,將 env
加入 tasklist
的佇列中。接著就在任務間來回切換並更新佇列,直到執行完畢。
我們將任務改為三個,並把執行中的 i
顯示出來確認變數的變化狀況:
我發現區域變數 i
在不同函式運行間竟然會互相影響!以本次執行結果觀察:任務切換過程中變數 i
受到任務迴圈的運算影響而累加,並且在Task 2
完成程式後,變數 i
存值變成未定義的存值,task_switch
回到 Task 0,1
時,發現大於區域變數 static n
進而跳出迴圈。
從 gdb 追蹤區域變數 i
,發現三個函式對 i
的地址是相同的:
從 IBM 文件中可以看到這相關的警告:
- The values of register variables are unpredictable.
- Nonvolatile auto variables that are changed between calls to the setjmp and longjmp functions are also unpredictable.
為何會有這樣的問題?從 setjmp(3) — Linux manual page 裡有說明:
The compiler may optimize variables into registers, and longjmp() may restore the values of other registers in addition to the stack pointer and program counter. Consequently, the values of automatic variables are unspecified after a call to longjmp() if they meet all the following criteria:
- they are local to the function that made the corresponding setjmp() call;
- their values are changed between the calls to setjmp() and longjmp(); and
- they are not declared as volatile.
當我把變數 i
設定為 vollatile int i
,仍然沒有解決上述問題。
如果我們把變數 i
改為 static
,執行結果如下:
任務間運行次數獨立,我想這才是我們預期看到的結果。
5
: ACCESS_ONCE
當一段程式被指派的變數只有被指派卻未被使用時,比如在一個迴圈中使用指派 lock owner
時,這有可能被編譯器優化。使用ACCESS_ONCE
來防止該程式被優化而無法達到預期的 happens before。
如何防止執行的 x 被優化?這裡透過巨集假設讓 x 被指派一個新的變數,讓並設定 volatile
告訴編譯器這個變數不須優化。
這裡我們遇到一個問題是:不確定此變數的型態與大小為何,如果被指派的大小不一時,可能導致 overflow。我們透過 union
的特性來讓將要被指派的 __u.__c
能夠和 __u.__val
的配置空間一樣大:
union:
其中所有成員都共用相同的記憶體位置。 這項定義表示,在任何指定的時間, union 都只能在其成員清單中包含一個以上的物件。 這也表示無論有多少成員 union ,一律會使用足夠的記憶體來儲存最大的成員。
最後我們執行我們要做的事:我們根據其大小,轉型並指派固定大小空間的型別,並使用 volatile
達到目的。
DECL0
: char[1] __c
許多程式撰寫者在撰寫 Read-Write
的多執行緒任務時,為了維持預期的 Happens Before 的結果,會透過 volatile
的型態使編譯器不做優化,得以保持程式的 atomic
不可分割。然而這樣卻是個不好的習慣。
首先如果在正確的方式使用 spinlock 下,應該能夠排除程式執行的正確性,反而使用 volatile
將會使速度被拖慢。
另一個可能導致系統優化的情境是 busy-waiting 的索要變數資訊情況:此文章建議我們可以使用 cpu_relax();
的指令,可以降低 CPU 功耗,也讓編譯器不會判斷這是浪費的指派行動,而將指令搬出去的狀況。那也不需要使用 volatile
。