最近同事分享了一個一小段在新增 node 到 singly linked list 時不用判斷 head/tail 是否為 nullptr 的寫法,因為滿有趣的,所以分享一下
概念上來說其實滿簡單的,一般來說的 signly linked list 的寫法會像下面這樣 (極度簡化)
struct Node {
void* data;
Node* next;
};
class List
{
public:
List() : head(nullptr), tail(nullptr) {}
~List() {
while (this->head) {
auto tmp = this->head;
this->head = this->head->next;
delete tmp;
}
}
void addTail() {
auto node = new Node{nullptr, nullptr};
if (this->tail) {
this->tail->next = node;
this->tail = node;
}
else {
this->head = this->tail = node;
}
}
private:
Node* head;
Node* tail;
};
我們需要判斷 head/tail 是否為 nullptr 來採取不同的處理方式。
當然,這可以藉由 dummy head 的作法來避開,只是如果 Node 太大,dummy head 就會占用多餘的記憶體;採用如上面範例的方式,把 data 放在別的空間,再用 pointer 指過去雖然可以避開這問題,但也會在每次要存取資料時多了一次的 memory reference 的時間。
brachless 的作法用了一個很巧妙的方式做到類似 dummy head 的效果,基本想法就是把 List 本身當成一個 dummy head。反正我們不會真的用到 dummy head 的資料欄位,只會用到 dummy head 的 next pointer,那就不如把 List 的 head 當成 dummy head 的 next pointer 就好。如下:
class List
{
public:
List() : head(nullptr) {
constexpr auto offset = sizeof(Node) - sizeof(Node::next);
this->tail = reinterpret_cast<node>(&(this->head)) - offset;
}
~List() {
while (this->head) {
auto tmp = this->head;
this->head = this->head->next;
delete tmp;
}
}
void addTail() {
auto node = new Node{nullptr, nullptr};
this->tail->next = node;
this->tail = node;
}
private:
Node* head;
Node* tail;
};
假設 List 的 head pointer 變成 dummy head 的 next pointer,那 List 的 dummy head 就是以 head 為基準,往前一段 offset,然後讓 tail 指向那邊,如此一來可以讓 tail->next 剛好就會是 List 的 head pointer 本身。
我們也可以利用 Compiler Explore 看 assembly 的差異:branchless v.s. traditional
雖然滿有趣的,不過現在 compiler 跟硬體的 branch prediction 都做得滿好的,實際使用上如果不是很常遇到 List 會變成空的,performance 應該會看不出明顯差異。
另一個值得考量的問題是 branchless 的寫法在 initialize/clear/delete 都要特殊處理,一有疏漏很容易造成 bug 造成後續維護上的困擾 (比方說 legacy code XD)。是以雖然有趣,但我覺得絕大多數情形況下不如照著常見的寫法走比較不會出問題。
完整的 code 如下
沒有留言:
張貼留言