Cấu trúc dữ liệu và Giải thuật Bài 129: Cây thống trị (Dominator Tree - Thuật toán Lengauer-Tarjan) - Phân tích cấu trúc kiểm soát luồng thực thi trong trình biên dịch.
Trong kỹ thuật xây dựng trình biên dịch hiện đại (như LLVM, GCC hay Go compiler), mã nguồn sau khi phân tích cú pháp sẽ được hạ cấp thành biểu diễn trung gian (IR) dưới dạng Đồ thị dòng điều khiển (Control Flow Graph - CFG). Mỗi nút trong CFG là một khối lệnh cơ bản (Basic Block), và các cạnh có hướng thể hiện các bước rẽ nhánh điều kiện hoặc vòng lặp.
Để thực hiện các kỹ thuật tối ưu hóa mã máy then chốt như:
-
Xây dựng dạng gán đơn tĩnh Static Single Assignment (SSA form) bằng cách đặt các hàm (phi-nodes).
-
Phát hiện và tối ưu hóa vòng lặp tự nhiên (Natural Loops / Loop Invariant Code Motion).
-
Loại bỏ mã chết (Dead Code Elimination).
Trình biên dịch bắt buộc phải trả lời câu hỏi: "Khối lệnh nào chắc chắn phải được thực thi trước khi luồng chương trình chạm tới khối lệnh ?" Cấu trúc giải quyết bài toán này là Cây thống trị (Dominator Tree), và phương pháp tính toán tối ưu nhất là Thuật toán Lengauer-Tarjan với độ phức tạp gần như tuyến tính hoặc .
1. Bản chất của Khái niệm Thống trị (Dominance) trong Đồ thị có hướng
Cho một đồ thị có hướng với một đỉnh gốc cố định (điểm bắt đầu thực thi của hàm/chương trình):
-
Thống trị (Dominates): Đỉnh được gọi là thống trị đỉnh (ký hiệu ) nếu mọi đường đi có hướng từ gốc đến đều phải đi qua . Theo định nghĩa, mỗi đỉnh luôn tự thống trị chính nó ().
-
Thống trị thực sự (Strictly Dominates): thống trị thực sự nếu và .
-
Thống trị trực tiếp (Immediate Dominator - ): Đỉnh là đỉnh thống trị trực tiếp của (ký hiệu ) nếu:
-
thống trị thực sự .
-
Mọi đỉnh khác thống trị thực sự đều thống trị .
-
Mỗi đỉnh có thể tiếp cận được từ đều có duy nhất một đỉnh thống trị trực tiếp.
-
-
Cây thống trị (Dominator Tree): Nếu ta tạo các cạnh có hướng cho mọi , ta thu được một cây có gốc tại . Trong cây này, quan hệ tổ tiên - con cháu phản ánh quan hệ thống trị trong đồ thị gốc: thống trị khi và chỉ khi là tổ tiên của trên Dominator Tree.
CFG (Đồ thị điều khiển) Dominator Tree
(1) (1)
/ \ / | \
(2) (3) (2)(3)(4)
\ / |
(4) (5)
|
(5)
(Đường đi tới 4 có thể qua 2 hoặc 3,
nên idom(4) = 1 chứ không phải 2 hay 3)
2. Nút bán thống trị (Semidominator) và Định lý Lengauer-Tarjan
Nếu kiểm tra quan hệ thống trị bằng cách xóa từng đỉnh rồi chạy BFS/DFS kiểm tra tính liên thông đến , chi phí sẽ là — quá chậm đối với các hàm có hàng nghìn khối lệnh.
Thomas Lengauer và Robert Tarjan (1979) đã phát minh ra khái niệm Nút bán thống trị (Semidominator - ) đóng vai trò cầu nối trung gian:
Thứ tự thăm DFS (DFN)
Thực hiện duyệt DFS trên xuất phát từ , gán cho mỗi đỉnh một chỉ số thời gian .
- Nếu , được thăm trước .
Định nghĩa Semidominator ()
là đỉnh có nhỏ nhất sao cho tồn tại một đường đi thỏa mãn:
Nói cách khác, là điểm xuất phát cao nhất có thể "đi vòng qua" các đỉnh chưa hoàn thành của cây DFS để chạm tới .
Định lý Then chốt Lengauer-Tarjan
Với mỗi đỉnh :
-
luôn là một tổ tiên của trên cây DFS.
-
luôn là một tổ tiên của (hoặc chính là ).
-
Xét tất cả các đỉnh nằm trên đường đi từ xuống trên cây DFS (không bao gồm ). Gọi là đỉnh có nhỏ nhất:
-
Trường hợp 1: Nếu , thì:
-
Trường hợp 2: Nếu , thì:
-
3. Quy trình 3 giai đoạn của Thuật toán Lengauer-Tarjan
Thuật toán kết hợp cấu trúc Disjoint Set Union (DSU) có nén đường đi (Path Compression) để duy trì thông tin giá trị nhỏ nhất trên chuỗi tổ tiên:
-
Giai đoạn 1 (DFS Tree Construction):
- Duyệt DFS từ gốc , gán nhãn , lưu mảng ánh xạ ngược
rev[dfn] = u, và xác định nút cha trên cây DFS.
- Duyệt DFS từ gốc , gán nhãn , lưu mảng ánh xạ ngược
-
Giai đoạn 2 (Tính và thô theo thứ tự DFN giảm dần):
-
Duyệt các đỉnh từ lùi về :
-
Xét mọi cạnh ngược : tính dựa trên (nếu thăm trước ) hoặc (nếu thăm sau ).
-
Đưa vào danh sách chờ .
-
Nối vào tập hợp DSU của .
-
Duyệt qua tất cả các đỉnh : dùng hàm
evalcủa DSU để kiểm tra . Áp dụng định lý Lengauer-Tarjan để gán hoặc tạm thời gán .
-
-
-
Giai đoạn 3 (Hoàn thiện theo thứ tự DFN tăng dần):
-
Duyệt các đỉnh từ đến :
- Nếu , hoàn thiện bằng cách gán .
-
4. Hiện thực hóa Thuật toán Lengauer-Tarjan bằng Golang
Đoạn mã Golang dưới đây cài đặt hoàn chỉnh thuật toán Lengauer-Tarjan để xây dựng Dominator Tree cho một đồ thị có hướng bất kỳ:
Go
package main
import (
"bufio"
"fmt"
"os"
)
type DominatorTree struct {
n int
adj [][]int // Danh sách kề xuôi
radj [][]int // Danh sách kề ngược
dfn []int
rev []int
timer int
parent []int
sdom []int
idom []int
// Cấu trúc DSU hỗ trợ nén đường đi và tìm min-sdom
dsuParent []int
dsuMin []int
bucket [][]int // Chứa danh sách các đỉnh có cùng sdom
domAdj [][]int // Danh sách kề của Dominator Tree kết quả
}
func NewDominatorTree(n int) *DominatorTree {
dt := &DominatorTree{
n: n,
adj: make([][]int, n+1),
radj: make([][]int, n+1),
dfn: make([]int, n+1),
rev: make([]int, n+1),
parent: make([]int, n+1),
sdom: make([]int, n+1),
idom: make([]int, n+1),
dsuParent: make([]int, n+1),
dsuMin: make([]int, n+1),
bucket: make([][]int, n+1),
domAdj: make([][]int, n+1),
}
for i := 1; i <= n; i++ {
dt.sdom[i] = i
dt.dsuParent[i] = i
dt.dsuMin[i] = i
}
return dt
}
func (dt *DominatorTree) AddEdge(u, v int) {
dt.adj[u] = append(dt.adj[u], v)
dt.radj[v] = append(dt.radj[v], u)
}
// Giai đoạn 1: DFS đánh số DFN và xác định cây khung DFS
func (dt *DominatorTree) dfs(u int) {
dt.timer++
dt.dfn[u] = dt.timer
dt.rev[dt.timer] = u
for _, v := range dt.adj[u] {
if dt.dfn[v] == 0 {
dt.parent[v] = u
dt.dfs(v)
}
}
}
// DSU Find kết hợp Path Compression và duy trì đỉnh có sdom nhỏ nhất
func (dt *DominatorTree) find(u int) int {
if dt.dsuParent[u] == u {
return u
}
root := dt.find(dt.dsuParent[u])
if dt.dfn[dt.sdom[dt.dsuMin[dt.dsuParent[u]]]] < dt.dfn[dt.sdom[dt.dsuMin[u]]] {
dt.dsuMin[u] = dt.dsuMin[dt.dsuParent[u]]
}
dt.dsuParent[u] = root
return root
}
func (dt *DominatorTree) eval(u int) int {
dt.find(u)
return dt.dsuMin[u]
}
// Xây dựng Dominator Tree xuất phát từ gốc root: O(E log V)
func (dt *DominatorTree) Build(root int) {
dt.timer = 0
dt.dfs(root)
// Giai đoạn 2: Tính toán sdom và xử lý bucket theo thứ tự DFN giảm dần
for i := dt.timer; i >= 2; i-- {
w := dt.rev[i]
// 1. Tính sdom(w) từ các cạnh ngược đi vào w
for _, v := range dt.radj[w] {
if dt.dfn[v] == 0 {
continue // Đỉnh không chạm tới được từ root
}
var candidate int
if dt.dfn[v] < dt.dfn[w] {
candidate = v
} else {
uMin := dt.eval(v)
candidate = dt.sdom[uMin]
}
if dt.dfn[candidate] < dt.dfn[dt.sdom[w]] {
dt.sdom[w] = candidate
}
}
dt.bucket[dt.sdom[w]] = append(dt.bucket[dt.sdom[w]], w)
p := dt.parent[w]
dt.dsuParent[w] = p
// 2. Xử lý các đỉnh đang chờ tại bucket của p
for _, v := range dt.bucket[p] {
uMin := dt.eval(v)
if dt.sdom[uMin] == dt.sdom[v] {
dt.idom[v] = dt.sdom[v]
} else {
dt.idom[v] = uMin // Trì hoãn xử lý sang Giai đoạn 3
}
}
dt.bucket[p] = dt.bucket[p][:0]
}
// Giai đoạn 3: Hoàn thiện idom theo thứ tự DFN tăng dần
for i := 2; i <= dt.timer; i++ {
w := dt.rev[i]
if dt.idom[w] != dt.sdom[w] {
dt.idom[w] = dt.idom[dt.idom[w]]
}
// Thêm cạnh vào Dominator Tree kết quả
dt.domAdj[dt.idom[w]] = append(dt.domAdj[dt.idom[w]], w)
}
}
func main() {
// Giả lập Đồ thị dòng điều khiển (CFG) gồm 6 khối lệnh (1 đến 6)
// Gốc bắt đầu chương trình: khối 1
n := 6
root := 1
dt := NewDominatorTree(n)
// Cấu trúc phân nhánh điều kiện và vòng lặp:
// 1 -> 2, 1 -> 3
// 2 -> 4, 3 -> 4
// 4 -> 5, 5 -> 4 (vòng lặp giữa 4 và 5)
// 4 -> 6
dt.AddEdge(1, 2)
dt.AddEdge(1, 3)
dt.AddEdge(2, 4)
dt.AddEdge(3, 4)
dt.AddEdge(4, 5)
dt.AddEdge(5, 4)
dt.AddEdge(4, 6)
dt.Build(root)
writer := bufio.NewWriter(os.Stdout)
defer writer.Flush()
fmt.Fprintln(writer, "Kết quả Dominator Tree (idom của từng đỉnh):")
for i := 1; i <= n; i++ {
if i == root {
fmt.Fprintf(writer, "Đỉnh %d (Gốc CFG): Không có idom\n", i)
} else if dt.dfn[i] != 0 {
fmt.Fprintf(writer, "Đỉnh %d: idom = %d\n", i, dt.idom[i])
} else {
fmt.Fprintf(writer, "Đỉnh %d: Không tiếp cận được từ gốc\n", i)
}
}
fmt.Fprintln(writer, "\nCấu trúc cây thống trị (Cha -> Các con trực tiếp):")
for i := 1; i <= n; i++ {
if len(dt.domAdj[i]) > 0 {
fmt.Fprintf(writer, "Nút %d thống trị trực tiếp: %v\n", i, dt.domAdj[i])
}
}
}
5. Phân tích độ phức tạp thuật toán
| Thành phần | Độ phức tạp thời gian | Ghi chú |
|---|---|---|
| DFS Tree Traversal | Thăm mỗi đỉnh và cạnh đúng 1 lần | |
| Tính toán & Thao tác DSU | Mỗi cạnh ngược được duyệt 1 lần; DSU nén đường đi tốn | |
| Hoàn thiện (Giai đoạn 3) | Duyệt tuyến tính qua mảng DFN | |
| Tổng thể thuật toán | Cài đặt chuẩn mực, ngắn gọn và tối ưu cache | |
| Bộ nhớ phụ trợ | Mảng phẳng lưu trữ DFN, DSU và danh sách kề ngược |
Nếu áp dụng kỹ thuật cân bằng DSU nâng cao kết hợp hàm Ackermann ngược (), độ phức tạp lý thuyết có thể đạt , tuy nhiên trong thực tế cài đặt trên trình biên dịch, phiên bản sử dụng mảng phẳng có hằng số ẩn nhỏ hơn và chạy nhanh hơn đáng kể.
6. Ứng dụng thực tế trong Trình biên dịch & Hệ thống
-
Xây dựng Dạng gán đơn tĩnh (SSA Construction via Dominance Frontier): Biên giới thống trị (Dominance Frontier) của một nút được tính toán trực tiếp dựa trên Dominator Tree, giúp trình biên dịch xác định vị trí tối thiểu cần chèn các hàm khi quản lý các phiên bản biến số.
-
Nhận diện Vòng lặp tự nhiên (Natural Loop Detection): Một cạnh ngược trong CFG tạo thành một vòng lặp tự nhiên khi và chỉ khi . Đỉnh được xác định là Loop Header duy nhất của vòng lặp.
-
Phân tích Điểm yếu mạng lưới (Network Single-Point-of-Failure): Trong đồ thị định tuyến có hướng, mọi đỉnh nằm trên đường đi từ gốc đến trên Dominator Tree đều là các điểm thắt cổ chai bắt buộc. Nếu bất kỳ nút nào trong chuỗi này bị sập, nút sẽ mất kết nối hoàn toàn khỏi mạng.
All Rights Reserved