owned this note
owned this note
Published
Linked with GitHub
# Merge-sort-concurrent
##### tag `Yen-Kuan Wu` `HahaSula` `mergesort-concurrent` `data strcuture`
# 目標
修改 data structure 讓 mergesort 能夠對 insert 和 delete 會更友善。
# 組員
## 黃呂源
- [ ]~~lock-free 想法?冼鏡光投影片閱讀失敗 concurrentLL 失敗~~
- [ ]~~atom memory access?~~
- [x]分main_WorkQueue,thread_worker,roundrobin)
>先將workingQueue & dispatch queue分開 讓thread content 分散
>debug中
## 吳彥寬
- [x]trace ==mergesort==、thread 的 code
- [ ]code refactor: mergesort 的主體我將它們下外移(進行中...)有點小麻煩R
> 暫緩,我發現盲目下去改會改不好,先熟悉 code。[name=Yen-Kuan Wu]
- [x]針對 lock 和 time 的 benchmark ~~(進行中,debug)~~
- [ ]加上 condition variable
- [ ]找看看有沒可以改善的 lock
> 當我們做出 lock 時看到 contention 並不高,要調整測試條件[name=Yen-Kuan Wu]
- [ ]mergesort 熟悉完 lock 狀況後,分析可否使用不同的 data structure 來改寫
---
# Code 觀察
## main.c
main.c 整體架構如下
![](https://i.imgur.com/T9cJCuO.png)
觀察四條支線
* `list_add` 簡單的 init list
* `tpool_init` 簡單的 init threadpool(但裏面很難...囧)
>`tpool_init(pool, thread_count, task_run)` 所 pool 有吃到 `task_run` 這個 function [name=Yen-Kuan Wu]
* `cut` 可以說是這份 mergesort 的精華,我們看到他有 call 到 `merge` 和 `tqueue_push`,而 `push` 這個動作即是將 task 丟進 threadpool 裡
```clike=
new_task->func = cut;
new_task->arg = the_list;
tqueue_push(pool->queue, new_task);
```
>所以 `cut` 也被丟到 `pool` 裡去了[name=Yen-Kuan Wu]
* `tpool_free` 這裡面有實作 `tqueue_free` 等待所有任務做完
* 我們發現這整份 code 的切割方式很簡單,即是 一個 threadpool 當作執行任務的載體,而且特別有丟入 `task_run` 進去,其他 task 就直接丟進去執行,並且使用 lock 的方式確保正確性。
>看完整體架構跟我們的目標後,我們想到要修改這份 code 的切入點應該是,tqueue 是 link-list 和 mergesort 用的也是 link-list,我們將從這兩點切入,先熟悉 code 再提出改善。[name=Yen-Kuan Wu]
* 整份 main.c 裡有一些變數看不太懂
- [ ] `max_cut = thread_count * (thread_count <= data_count) + data_count * (thread_count > data_count) - 1;`
>~~計算全部thread切了幾刀,兩個data只需要切一刀~~[name=HahaSula]
>這我有點聽不懂耶,等等我們討論一下[name=Yen-Kuan Wu]
- [ ] `thread_data.cuthread_count = 0;`
>~~判斷cutthread_count = max cut 才會把cut轉成merge task push 到tqueue裏面~~[name=HahaSula]
>更正cut為把整分資料先大刀切給thread(讓多個thread做merge_sort 去切小塊)
>再給merge_sort下去切成實際被sort的的單位
---
### static void *task_run(void *data)
* 簡單的 while 迴圈,`pop` task 出來做
- [x] 這邊我好奇的是,如果 pool 剛 init 時是沒有 task 的吧,那為什麼不會直接結束呢? 我想這邊應該要多增加一個解說或是一個 function 來說告知,現在 queue 是屬於什麼狀態,可以 free 還是說只是沒有沒有任務進來。
>沒有task並不回造成break,task->Fun = NULL才會break;[name=HahaSula]
>感謝! 我在想我們的改進方向,或許可以朝查詢這個 task queue 的狀態來發展。[name=Yen-Kuan Wu]
---
### void cut ( void * data )
![](https://i.imgur.com/meHgk8C.png)
#### 觀察
* 我們發現 `cut` `merge` 都有 call `tqueue_push`,也就是任務有分 `cut` 和 `merge`(這觀察很像是廢話...)
* 再來是 `merge_sort` 這看不出來了,再來就是 trace 整份 code 了
* 首先看到我們第一個 critical section
```clike=
pthread_mutex_lock(&(thread_data.mutex));
int cutLocal = thread_data.cuthread_count;
if (list->size > 1 && cutLocal < max_cut) {
++thread_data.cuthread_count;
pthread_mutex_unlock(&(thread_data.mutex));
```
> 這邊我會們會發現,這邊很簡單就可以改成 atomic 了,由於裡面用到的共用變數只有 `thread_data.cuthread_count`(mutex lock 是針對 `thread_data`)[name=Yen-Kuan Wu]
很順利的我們改出了第1版 lock-free 的改進
```clike=
struct {
//pthread_mutex_t mutex;
_Atomic int cuthread_count;
} thread_data;
```
- [ ] 這邊我再改 code 時發現,我們根本沒有用到 list 的 lock
```clike=
struct {
pthread_mutex_t mutex;//FIXME: No use in code
llist_t *list;
} tmp_list;
```
>而且有一個部份的 code 很奇怪,明明 critical section 綁的是 tmp_list,可是卻 lock thread_data.mutex[name=Yen-Kuan Wu]
>有趣的是thread_data.mutex 已經cut完不會再被前面使用所以好像不會衝突[name=HahaSula]
```clike=
[main.c: 73: merge()]
//pthread_mutex_lock(&(thread_data.mutex));FIXME:No need lock
llist_t *tmpLocal = tmp_list.list;
if (!tmpLocal) {
tmp_list.list = list;
//pthread_mutex_unlock(&(thread_data.mutex));
} else {
tmp_list.list = NULL;
//pthread_mutex_unlock(&(thread_data.mutex));
```
>- [x] 目前我是將其給 comment 掉了,但是是否需要換成 tmp_list.mutex 這我還不能確定。[name=Yen-Kuan Wu]
>>我自己解掉他了,我發現這有可能發生 ABA 的問題,所以是需要 lock 的,那這邊一樣,我會想要改成 atomic 的版本[name=Yen-Kuan Wu]
>```clike=
>struct {
> //pthread_mutex_t mutex;//FIXME: No use in code
> llist_t * _Atomic list;
>} tmp_list;
>```
>>[name=Yen-Kuan Wu]
- [ ] 經過我這個 Atomic 的修改,我發現我只有解決了不會有 double assign 而沒有解決順序的部份,這可能還要再研究一下,也許要做個 heavy test 來檢查正確性,也許要將這一段 code 都改成 atomic
- [ ] heavy test for correction
# merge & cut 觀察
#### merge分兩階段
thread1 merge會把拿到的list放到tmp_list.list裏面
thread2 merge會把tmp_list.list的list跟傳入的list做merge_list
將return list做一個merge task push回去tqueue中(把tmp_list.list清空給下一個merge)
(結果:每吃兩個merge做一次merge_list)
(不知道會不會有merge1 跟merge2 size不一的情況存在)
> 呂源,麻煩你詳細一點,不然我也看不懂= =,至少讓我知道那幾個 function 再做什麼或者是他整體架構再幹嘛[name=Yen-Kuan Wu]
> 簡單來講,你就要把我當作一個有一點基礎,而且只有略窺過這份 code,不用 detail 但要跟我說你怎麼觀察到的再哪個 fucntion裡,或者能簡介一下這份code 的架構或 functio 再幹嘛。[name=Yen-Kuan Wu]
## thread.c 觀察
condition variable沒有使用
(如果要修改tqueue的lock不知道還有沒有要用)
### 程式描述
thread pool init
cut 將資料均分給各個thread(大刀)
各個thread 把自己的資料做merge sort(分別sort)
兩個兩個sort好的thread 合併list(sort完的thread 用merge合併)
## list.c
* `node_t`
```clike=
typedef intptr_t val_t;
typedef struct node {
val_t data;
struct node *next;
} node_t;
```
>至於 `intptr_t` 是指向 int 的 pointer 資料閱讀有解說。[name=Yen-Kuan Wu]
* `llist_t`
```clike=
typedef struct llist {
node_t *head;
uint32_t size;
} llist_t;
```
>我認為這個命名不是很漂亮,~~也許作者的意思是 leader list 吧,我猜,這是需要 refector 的部份。~~[name=Yen-Kuan Wu]
>>我錯了,linux kernel 裡就是用 llist 來代表 link list...
>- [x]這邊的 szie 需要確認一下他的用處為何,如果只是 link list 照裡來說是不需要 size 的[name=Yen-Kuan Wu]
>>這邊的 size 能用來幫助 check index 是否超過,就不用跑到底發現沒有。[name=Yen-Kuan Wu]
>- [x] 修改 llist_t 命名
>> 算了吧,linux kernel 都這樣用了...[name=Yen-Kuan Wu]
* `node_t *new_node(val_t val, node_t *next)`
> 這邊很特別的是,新創一個node插入再開頭,所以參數命名沒有錯,的確是新創一個 node 且 assign 之後 node->next 就會接到 next 去。[name=Yen-Kuan Wu]
>> 剛好看到一個用法 new_node(val, NULL),這樣的的確就是 new node 了。[name=Yen-Kuan Wu]
> 這不光光只有 new node 而且還有串接,所以看 function 名稱是看不出來的。[name=Yen-Kuan Wu]
> - [ ] refector,更好的名稱或者拆開
* `llist_t *list_new()`
> 基本的 init list[name=Yen-Kuan Wu]
* `int list_add(llist_t *the_list, val_t val)`
> 簡單的 append[name=Yen-Kuan Wu]
* `node_t *list_get(llist_t *the_list, uint32_t index)`
> output index 的 node 的 value,所以這邊會用到 size 先check index 是否大於 size 如果有就return NULL [name=Yen-Kuan Wu]
## 改進 link list 的想法
* 觀察完上面的 code 我們會發現他的缺點就是在,search 的時間,因為你要用 link 一個一個移到要找的地方
> 1. arrary 的方式如何呢? 但 arrary 的缺點就是 new node 不太方便,目前想法是,可以使用 link list + arrary 的方式,但是這樣會讓 mergesort 很難改,待考慮[name=Yen-Kuan Wu]
> 2. tree 的方式如何呢? 當然我們可以直接實作 heap sort,但假如我們把他用在 mergesort 也許也會有好處,想像right leaf 跟 left leaf 即是一個分割,這可以考慮看看[name=Yen-Kuan Wu]
> - [ ] survey linux kernel 的 alg 和 struct 或許裏面有很多我們沒想到的特殊演算法或 struct[name=Yen-Kuan Wu]
# Linux Kernel data structure survey
主要參考 <`lourie`> 在 FB 裡貼出來的連結(http://cstheory.stackexchange.com/questions/19759/core-algorithms-deployed/),他整理了 linux kernel 裡使用到的重要 data structure 和 algorithm
:::info
首先我發現,Linux kernel 裡的 comment 居然都有符合 Doxygen 的格式,代表我能使用 doxygen 輔助我了解 code 的相依性。
:::
# 實驗
## 極端狀況測試
- [x] 負的可不可以排? O
- [x] thread_num > data_count ? O
- [x] 吃得下 35萬個 data 嘛? O
```clike=
(for i in {1..350000};do echo $RANDOM;done )| ./sort 4 350000
```
- [x] 一個 thread 能跑嘛? O
- [ ] 超過 int 大小的能跑? X
## Dispatch queue實作 Data 1000 thread 1 2 4 8 16(仍然有mutex)
```shell=
ori 版本
mergesort for 1000 data with 1 threads take 1509 us
mergesort for 1000 data with 2 threads take 1001 us
mergesort for 1000 data with 4 threads take 1424 us
mergesort for 1000 data with 8 threads take 1103 us
mergesort for 1000 data with 16 threads take 1499 us
```
**average = 926**
![](https://i.imgur.com/FuGCgZE.png)
**average = 3027**
![](https://i.imgur.com/Esg8HX2.png)
**average = 42540**
![](https://i.imgur.com/V7D19kn.png)
**average = 181584**
![](https://i.imgur.com/Vxqzzz2.png)
**average = 512155**
![](https://i.imgur.com/ZXzd6yh.png)
**mutrace**
* ori:
```shell
Mutex # Locked Changed Cont. tot.Time[ms] avg.Time[ms] max.Time[ms] Flags
2 419 144 112 0.074 0.000 0.001 Mx.--.
0 40 25 9 0.007 0.000 0.001 M-.--.
1 29 24 4 0.005 0.000 0.000 M-.--.
||||||
/|||||
Object: M = Mutex, W = RWLock /||||
State: x = dead, ! = inconsistent /|||
Use: R = used in realtime thread /||
Mutex Type: r = RECURSIVE, e = ERRRORCHECK, a = ADAPTIVE /|
Mutex Protocol: i = INHERIT, p = PROTECT /
RWLock Kind: r = PREFER_READER, w = PREFER_WRITER, W = PREFER_WRITER_NONREC
```
* after:
```
Mutex # Locked Changed Cont. tot.Time[ms] avg.Time[ms] max.Time[ms] Flags
4 41821 22 19 8.193 0.000 0.018 M-.--.
1 109758 8 8 20.845 0.000 0.049 Mx.--.
0 100650 8 8 21.070 0.000 2.847 Mx.--.
3 83146 6 6 19.873 0.000 5.192 Mx.--.
2 57248 6 3 10.219 0.000 0.073 Mx.--.
6 25 8 1 0.005 0.000 0.001 M-.--.
5 13 11 0 0.005 0.000 0.001 M-.--.
||||||
/|||||
Object: M = Mutex, W = RWLock /||||
State: x = dead, ! = inconsistent /|||
Use: R = used in realtime thread /||
Mutex Type: r = RECURSIVE, e = ERRRORCHECK, a = ADAPTIVE /|
Mutex Protocol: i = INHERIT, p = PROTECT /
RWLock Kind: r = PREFER_READER, w = PREFER_WRITER, W = PREFER_WRITER_NONREC
```
**cache miss**
* ori
```shell=
Performance counter stats for './sort 1 1000':
8516 cache-misses # 25.416 % of all cache refs
3,3507 cache-references
235,0391 instructions # 0.90 insns per cycle
262,4563 cycles
0.003585538 seconds time elapsed
mergesort for 1000 data with 2 threads take 714 us
Performance counter stats for './sort 2 1000':
6770 cache-misses # 19.269 % of all cache refs
3,5135 cache-references
257,8524 instructions # 0.96 insns per cycle
269,6694 cycles
0.002147490 seconds time elapsed
mergesort for 1000 data with 4 threads take 1020 us
Performance counter stats for './sort 4 1000':
5765 cache-misses # 12.535 % of all cache refs
4,5991 cache-references
386,5704 instructions # 0.76 insns per cycle
510,7594 cycles
0.002345743 seconds time elapsed
mergesort for 1000 data with 8 threads take 788 us
Performance counter stats for './sort 8 1000':
5714 cache-misses # 11.888 % of all cache refs
4,8066 cache-references
349,8934 instructions # 0.83 insns per cycle
422,7095 cycles
0.002090579 seconds time elapsed
mergesort for 1000 data with 16 threads take 1061 us
Performance counter stats for './sort 16 1000':
8564 cache-misses # 12.828 % of all cache refs
6,6762 cache-references
506,4661 instructions # 0.74 insns per cycle
687,1425 cycles
0.002585683 seconds time elapsed
```
* after:
```shell==
Performance counter stats for './sort 1 10000':
1,1637 cache-misses # 28.579 % of all cache refs
4,0719 cache-references
3036,3061 instructions # 1.03 insns per cycle
2941,6038 cycles
0.010364469 seconds time elapsed
mergesort for 10000 data with 2 threads take 3299 us
Performance counter stats for './sort 2 10000':
7315 cache-misses # 15.460 % of all cache refs
4,7316 cache-references
2587,7678 instructions # 1.05 insns per cycle
2460,9844 cycles
0.004680364 seconds time elapsed
mergesort for 10000 data with 4 threads take 20934 us
Performance counter stats for './sort 4 10000':
2,5720 cache-misses # 31.558 % of all cache refs
8,1501 cache-references
1,4593,1072 instructions # 0.86 insns per cycle
1,6953,7714 cycles
0.022214962 seconds time elapsed
mergesort for 10000 data with 8 threads take 140475 us
Performance counter stats for './sort 8 10000':
3,9885 cache-misses # 33.887 % of all cache refs
11,7699 cache-references
10,0577,8308 instructions # 0.82 insns per cycle
12,3025,4065 cycles
0.142290916 seconds time elapsed
mergesort for 10000 data with 16 threads take 505297 us
Performance counter stats for './sort 16 10000':
3,8847 cache-misses # 18.931 % of all cache refs
20,5208 cache-references
36,6412,2022 instructions # 0.80 insns per cycle
45,9637,9790 cycles
0.506762169 seconds time elapsed
```
>hi!簡單幫你們切割了一下數據,加了一些粗體字,應該會更一目了然!
>可以再想想怎麼呈現實驗結果,因為資料很多筆,或許嘗試使用表格整理會不錯!目前我也還沒有想到更好的方法啦>"<[color=red][name=課程助教]
# Benchmark
### TIMING
看到老師寫的 fixme,才想到裡面多餘的 printf 也是會造成 overhead
```clike=
/* FIXME: remove all all occurrences of printf and scanf
* in favor of automated test flow.
*/
```
* 新增 TIMING 的 flag 來計算時間
### Lock
我們使用老師給的 `mutrace` 來檢測
### benchmark 設計
這邊使用 bash script 來跑實驗,我設計將實驗用到的參數拉出來
* `bechmark.cfg`
```shell=
TEST_SET=(1 2 4 8 16)
TEST_DATA_NUM=10000
```
* benchmark-time.bash
```shell=
#!/bin/bash
. benchmark.cfg
for i in ${TEST_SET[@]}; do ./sort $i ${TEST_DATA_NUM}; done
```
# 資料閱讀
## Bash 指令
## GCC 指令
* 3.12 Options Controlling the Preprocessor
[-MF -MMD](https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html)
> 這邊我找到更好的連結 [ntu gcc解說](http://www.cmlab.csie.ntu.edu.tw/~daniel/linux/gcc.html),還有[gcc 中文手冊](http://www.cmlab.csie.ntu.edu.tw/~daniel/linux/cman/gcc.html)[name=Yen-Kuan Wu]
引用 `<kobeYu>`
```
-M
生成文件關聯的信息。包含目標文件所依賴的所有源代碼你可以用gcc -M hello.c來測試一下,很簡單。
-MM
和上面的那個一樣,但是它將忽略由#include<file>;造成的依賴關係。
-MD
和-M相同,但是輸出將導入到.d的文件裡面
-MMD
和-MM相同,但是輸出將導入到.d的文件裡面
-MF
指定輸出到某個檔案,否則預設是原始檔案檔名.d
```
> 參考:[kobeYu/mergesort-concurrency共筆](https://hackmd.io/s/Bydtpi4R)[name=Yen-Kuan Wu]
main.c 81
## mutrace
如果想要使用 mutrace 的話要在 gcc 後面加上 `-rdynamic`
>Make sure to build your application with -rdynamic to make the backtraces mutrace generates useful. [name=Arthur]
* 作者的 [blog](http://0pointer.de/blog/projects/mutrace.html)
```
Mutex # Locked Changed Cont. tot.Time[ms] avg.Time[ms] max.Time[ms] Flags
1 2734 1342 774 0.715 0.000 0.018 Mx.--.
0 80 55 21 0.019 0.000 0.001 M-.--.
2 61 52 2 0.017 0.000 0.000 M-.--.
||||||
/|||||
Object: M = Mutex, W = RWLock /||||
State: x = dead, ! = inconsistent /|||
Use: R = used in realtime thread /||
Mutex Type: r = RECURSIVE, e = ERRRORCHECK, a = ADAPTIVE /|
Mutex Protocol: i = INHERIT, p = PROTECT /
RWLock Kind: r = PREFER_READER, w = PREFER_WRITER, W = PREFER_WRITER_NONREC
```
>~~這邊有個問題是作者的用詞都是 how often 害我不知道他到底是次數還是時間,待我之後測試~~[name=Yen-Kuan Wu]
>就後續來看其面三項應該是幾次。(作者後面有說了= =)
* ==`Locked`== column tells how often the mutex was locked during the entire runtime of about 10s.
>這個 mutex ~~多常~~被 lock幾次。[name=Yen-Kuan Wu]
* ==`Changed`== column tells us how often the owning thread of the mutex changed.
>這我不太清楚,也許是 mutex 轉移。[name=Yen-Kuan Wu]
* ==`Cont.`== column tells us how often the lock was already taken when we tried to take it and we had to wait.
> 這個 lock 被 require 時是已經 taken了的次數。[name=Yen-Kuan Wu]
* ==`tot.Time` `avg.Time` `max.Time`== 我就不說明了,也就是 lock 的時間
*
- [ ] `addr2line` 一直都是亂碼
- [ ]閱讀 [More Mutrace](http://0pointer.net/blog/projects/mutrace2.html)
### 追蹤mutex
實際行數約在mutex_init下面一點
問題只有做tqueue threadcount的init
可是卻追蹤到3個mutex
``` shell
原始code
Mutex #0 (0x0xa5b870) first referenced by:
/usr/lib/mutrace/libmutrace.so(pthread_mutex_init+0xf2) [0x7fc9977cd4b2]
./sort(tqueue_init+0x38) [0x401356]
./sort(tpool_init+0x6a) [0x401596]
./sort(main+0x156) [0x401e33]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fc997204830]
$ addr2line -e sort 0x401356
/home/sula/workspace/mergesort-concurrent-ken/thread.c:16
$ addr2line -e sort 0x401596
/home/sula/workspace/mergesort-concurrent-ken/thread.c:80
$ addr2line -e sort 0x401e33
/home/sula/workspace/mergesort-concurrent-ken/main.c:185
```
# Atomic
## C11 atomic
閱讀[Atomic C11 PPT](https://www2.informatik.hu-berlin.de/~keil/docs/keil_-_c11_atomics_20140202.pdf)
[p.10]
* `_Atomic` int
```
_Atomic int* pi; // pointer to atomic int
int* _Atomic pi; // atomic pointer (to int)
```
> 這邊我參照這個想法做了一個 muti-thread counter,一個是有 atomic 另一個是沒有的,[code在這邊](https://github.com/c14006078/PracticeMakePerfect/tree/master/c11_atomic),而且事實比對有 atomic 會是正確的數值。[name=Yen-Kuan Wu]
[p.11]
* `_Atomic` struct 的 member 不能獨立 access
* 看不懂`must be copied to compatible object and then
members will be accessed individually`
## Atomic memory access
[__sync_add_and_fetch](https://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html)
問題?
編譯器最佳化OOO===>memory Barrier
[簡單範例blog](http://reborn2266.blogspot.tw/2013/03/memory-barrier-in-lock-api.html)
atomic memory 的指令會對share_list 更動,不知道share_list是什麼。
**在 barrier 之前的CPU 指令會保證比 barrier 之後的指令先完成。**
## Signal
## lock
wiki lock
* lock overhead
* 也就是只所有 lock 帶來的負擔,space、init/destroy time、time for reqiring and releasing lock ...
* lock contention
* 當一人握有 lock 時,另一人再進行 require
* wiki 有說到,儘量要讓 lock 鎖在細項,而不是整個 table,這樣也能有效減少競爭,但是我想的是越細索是
>在直播聊天室提問了這個問題,我們是想
* deadlock
## intptr_t ?
主要參考 [使用變數型別的良好習慣](http://b8807053.pixnet.net/blog/post/164224857-%E4%BD%BF%E7%94%A8%E8%AE%8A%E6%95%B8%E5%9E%8B%E5%88%A5%E7%9A%84%E8%89%AF%E5%A5%BD%E7%BF%92%E6%85%A3)
**一張圖看出端倪**
```
32 bit 64 bit
char 1 1
short 2 2
int 4 4
long 4 8
long long 8 8
pointer 4 8
```
```clike=
#include <stdio.h>
#include <stdint.h>
int main()
{
int i = 10;
int *p = &i;
intptr_t pp = (intptr_t)p;
printf("%p\n", p);
printf("%lx\n", pp);
printf("%p\n", &i);
return 0;
}
```
>這邊我們要考慮 ptr 是在 32bit 還 64bit 上,如果想把他轉成 int 的話,用 intptr_t 會比較 protable[name=Yen-Kuan Wu]
>裡頭還提到很多,這讓我思考我的變數應該要以更統一的方式來宣告,例如 int16_t 之類的,而不會取決於機器[name=Yen-Kuan Wu]