算法原理

每个容器的核心算法与数据结构图解


ccmap — 红黑树

红黑树是自平衡二叉搜索树,每个节点有红/黑颜色属性。ccmap 是侵入式实现——节点嵌入用户结构体。

使用场景

场景 说明
定时器管理 以超时时间为 key,first 即最近到期定时器。O(1) 取最小 + O(log n) 增删,替代线性扫描
连接跟踪 以 socket fd 为 key 管理 TCP 会话表,O(log n) 查找/更新/关闭,遍历有序(按 fd 排序)
路由表 IP 前缀为 key,最长前缀匹配用 find + prev/next 范围扫描
有序事件队列 事件按时间戳排序,begin→next 顺序处理,支持动态插入/取消(erase)

红黑树五条性质

  1. 节点非红即黑
  2. 根节点是黑色
  3. 所有叶子(NIL)是黑色
  4. 红色节点的两个子节点必须是黑色(无连续红)
  5. 从任意节点到其所有后代叶子的每条路径上,黑色节点数量相同(黑高一致)

插入流程

插入后可能违反性质 2 或 4,通过重新着色 + 旋转修复:

flowchart TD
    A["新节点 = 红色"] --> B{"父节点是黑色?"}
    B -->|是| C["✅ 完成,无需修复"]
    B -->|否| D{"叔叔节点是红色?"}
    D -->|是| E["重新着色:父+叔→黑,祖父→红"]
    E --> F["问题上移至祖父"]
    F --> B
    D -->|否| G{"当前节点是\n父的右孩子?"}
    G -->|是| H["左旋父节点"]
    H --> I["右旋祖父 + 重新着色"]
    G -->|否| I

    style A fill:#f96,stroke:#333
    style C fill:#6f6,stroke:#333
    style E fill:#ff9,stroke:#333
    style I fill:#9cf,stroke:#333

旋转操作

参数 x 为旋转轴心节点,y 为其子节点。旋转保持 BST 性质,仅改变局部指针。

graph TD
    subgraph 右旋["右旋 _rb_rot_right(m, x)"]
        subgraph Rbefore["Before"]
            Xr("x") --> Yr("y")
            Xr --> Gr("γ")
            Yr --> Ar("α")
            Yr --> Br("β")
        end
        subgraph Rafter["After"]
            Yr2("y") --> Ar2("α")
            Yr2 --> Xr2("x")
            Xr2 --> Br2("β")
            Xr2 --> Gr2("γ")
        end
        Rbefore --> Rafter
    end
    subgraph 左旋["左旋 _rb_rot_left(m, x)"]
        subgraph Lbefore["Before"]
            Xl("x") --> Al("α")
            Xl --> Yl("y")
            Yl --> Bl("β")
            Yl --> Gl("γ")
        end
        subgraph Lafter["After"]
            Yl2("y") --> Xl2("x")
            Yl2 --> Gl2("γ")
            Xl2 --> Al2("α")
            Xl2 --> Bl2("β")
        end
        Lbefore --> Lafter
    end

cchashmap — 链式哈希表

侵入式链式哈希表。节点缓存 hash 值避免重复计算。

使用场景

场景 说明
DNS 缓存 域名为 key,解析结果为 value,O(1) 查询 + 自动淘汰(外部维护 TTL)
会话存储 session ID → 用户数据,无序遍历,纯 O(1) 读写
去重过滤器 URL / 消息 ID 去重,快速判存在,无需排序
对象池 资源句柄 → 对象指针,频繁获取/归还,均摊 O(1)

结构

flowchart TD
    subgraph buckets["桶数组 (cchashmap_node_t **)"]
        B0["[0]"] --> N1["node {key:42, hash:42}"]
        B1["[1]"] --> NULL
        B2["[2]"] --> N2["node {key:18, hash:18}"] --> N3["node {key:34, hash:34}"]
        B3["[3]"] --> NULL
        B4["[4]"] --> N4["node {key:20, hash:20}"]
    end

    style B0 fill:#e8e8e8,stroke:#333
    style B1 fill:#e8e8e8,stroke:#333
    style B2 fill:#e8e8e8,stroke:#333
    style B3 fill:#e8e8e8,stroke:#333
    style B4 fill:#e8e8e8,stroke:#333

核心操作流程

flowchart TD
    subgraph insert["插入 set(k, v)"]
        I1["hash = hash_fn(k, seed)"] --> I2["idx = hash & (cap - 1)"]
        I2 --> I3["遍历 buckets[idx] 链表"]
        I3 --> I4{"找到重复?"}
        I4 -->|是| I5["❌ 返回 false, *out=已存在"]
        I4 -->|否| I6["头插: node→next = buckets[idx]"]
        I6 --> I7["buckets[idx] = node, size++"]
        I7 --> I8{"size/cap ≥ MAX_LOAD?"}
        I8 -->|是| I9["resize: 2× 扩容 + rehash"]
        I8 -->|否| I10["✅ 返回 true"]
    end

    subgraph find["查找 get(probe)"]
        F1["hash = hash_fn(probe, seed)"] --> F2["idx = hash & (cap - 1)"]
        F2 --> F3["遍历 buckets[idx] 链表"]
        F3 --> F4{"hash 匹配 AND equal?"}
        F4 -->|是| F5["✅ 返回节点指针"]
        F4 -->|否| F6["返回 NULL"]
    end

扩容 (Rehash)

flowchart LR
    subgraph old["旧桶 (cap=4)"]
        O0["[0]: A→D"]
        O1["[1]: 空"]
        O2["[2]: B→C"]
        O3["[3]: 空"]
    end
    subgraph new["新桶 (cap=8)"]
        N0["[0]:"]
        N1["[1]:"]
        N2["[2]: C→D"]
        N3["[3]:"]
        N4["[4]: A"]
        N5["[5]:"]
        N6["[6]: B"]
        N7["[7]:"]
    end
    old -->|"2×扩容, 重新 hash&7"| new

ccheap — D-ary 堆

D-ary 堆是二叉堆的泛化,每个节点有 D 个子节点(ccheap 支持 2/4/8)。

使用场景

场景 说明
任务调度器 priority 越小的任务越先执行,pop 取最高优先级,insert 添加新任务
定时器轮询 timeout 为 priority,peek 查看最近超时而不弹出,结合事件循环使用
Top-K 查询 维护大小为 K 的最小堆,遍历全量数据,堆顶即为第 K 大
事件驱动模拟 离散事件按时间戳排序,每次 pop 最早事件执行
A* 路径搜索 节点 cost 为优先级,ccheap_update 在发现更优路径时 O(log n) 更新已存在节点的代價

堆结构(以二叉堆为例)

flowchart TD
    R["[0] priority=1 (min)"] --> L["[1] priority=5"]
    R --> RI["[2] priority=3"]
    L --> LL["[3] priority=9"]
    L --> LR["[4] priority=7"]
    RI --> RL["[5] priority=4"]
    RI --> RR["[6] priority=8"]

    style R fill:#6f6,stroke:#333

插入 (上滤 / Sift-up)

flowchart TD
    A["新元素追加到数组尾部"] --> B["i = len - 1"]
    B --> C{"i > 0 且 parent < node?"}
    C -->|是| D["swap(i, parent)"]
    D --> E["i = parent"]
    E --> C
    C -->|否| F["✅ 完成"]

弹出 (下滤 / Sift-down)

flowchart TD
    A["取出 data[0] (堆顶)"] --> B{"len == 1?"}
    B -->|是| C["len--,返回堆顶"]
    B -->|否| D["data[0] = data[len-1], len--"]
    D --> E["i = 0"]
    E --> F{"子节点中有更大的?"}
    F -->|是| G["swap(i, 最大子节点)"]
    G --> H["i = 最大子节点索引"]
    H --> F
    F -->|否| I["✅ 完成,返回堆顶"]

### Decrease-key / 节点更新 (可选)

A* / Dijkstra 等算法需要在搜索过程中降低已存在节点的优先级。
通过 `#define CCHEAP_NODE_INDEX <字段名>` 开启索引追踪,节点嵌入额外的
`size_t` 字段记录其在堆数组中的位置,提供 `ccheap_update` 操作。

```c
#define CCHEAP_NODE_INDEX heap_idx
#define CCHEAP_COMPARE(a, b) ((int64_t)((b)->cost - (a)->cost))
#include "ccheap.h"

struct search_node {
    ccheap_node_t hn;        // 内含 .heap_idx 字段
    int x, y;
    double g, f;
};

// 找到更优路径 → O(log n) 更新
n->f = new_f;
ccheap_update(&open_set, &n->hn);

算法流程:

零开销: 不定义 CCHEAP_NODE_INDEX 时,节点保持 8 字节,无额外指令。

对比懒删除方案:

方案 操作 堆大小 额外内存
懒删除(无宏) 发现更优路径时 push 新节点,pop 时跳过 stale 可能膨胀 0
CCHEAP_NODE_INDEX ccheap_update O(log n) 原地更新 紧凑 每个节点 +8B

### D-ary 子节点

| Arity | 子节点公式 | 编译期展开 |
| --- | --- | --- |
| 2 | `parent*2+1, parent*2+2` | 2 路 if |
| 4 | `parent*4+k+1` (k=0..3) | 4 路 if |
| 8 | `parent*8+k+1` (k=0..7) | 8 路 if |

> 子节点比较通过 `#if CCHEAP_ARITY_N > N` 编译期展开,无循环开销。

---

## cclink — 侵入式单向链表

### 使用场景

| 场景 | 说明 |
| --- | --- |
| **哈希桶链** | `cchashmap` 内部每个槽位的冲突链即可用 cclink 实现,纯 forward 遍历 |
| **空闲列表 (free list)** | 对象池中未分配块用单链串起,头取头放 O(1) |
| **LIFO 栈** | `push`=头插 O(1),`pop_front`=头删 O(1),无需双向指针 |
| **指令队列** | 简单 FIFO 不要求反向遍历的场景,内存开销最小(每个节点仅 1 指针) |

### 数据结构

```mermaid
flowchart LR
    H["head"] --> N1["node A"] --> N2["node B"] --> N3["node C"] --> NULL

操作流程


cclist — 侵入式双向链表

使用场景

场景 说明
LRU 缓存 访问时 remove + push_front O(1),淘汰时 pop_back O(1)
消息队列 生产者 push_back,消费者 pop_front,均为 O(1)
帧渲染链表 UI 控件/游戏对象按 Z-order 双向链接,支持 O(1) 插入/移除任意位置
多级队列 splice_back 将整个子队列原子移动到主队列,O(1) 无拷贝

数据结构

flowchart LR
    H["head sentinel\n(dummy)"] <--> N1["node A"] <--> N2["node B"] <--> T["tail sentinel\n(dummy)"]
    H -..-> N1
    T -..-> N2

操作流程


ccvector — 动态数组

值存储的连续内存数组,自动扩容。

使用场景

场景 说明
批量数据收集 遍历过程中 push_back 收集结果,最后一次性处理,利用 CPU 缓存局部性
栈 (LIFO) push_back / pop_back 实现,O(1) 均摊,连续内存无碎片
临时缓冲区 替代 malloc 管理动态数组,自动扩容无需手动 realloc
数值计算 稠密矩阵/向量按索引随机访问 O(1),比链表快 10-100×(缓存友好)

数据结构

扩容流程

均摊扩容:

flowchart TD
    A["push_back 调用"] --> B{"len == cap?"}
    B -->|否| C["data[len++] = elem\n✅ O(1)"]
    B -->|是| D["realloc: new_cap = cap * 2"]
    D --> E["复制旧元素到新内存"]
    E --> F["data[len++] = elem"]
    F --> G["✅ 均摊 O(1)"]

    style C fill:#6f6,stroke:#333
    style G fill:#ff9,stroke:#333

扩容策略:初始 cap=8,每次翻倍,均摊 O(1)。

排序

ccvector_sort 使用 C 标准库 qsort 对元素进行原地排序,O(n log n)。

二分查找

排序后可用 ccvector_bsearch 进行 O(log n) 二分查找。封装 C 标准库 bsearch

ccvector_sort(&v, my_compare);
int key = 42;
ccvector_node_t *p = ccvector_bsearch(&v, &key, my_compare);

比较器签名与 qsort/bsearch 一致(返回负数 → 键在元素前,正数 → 键在元素后,0 → 匹配)。

ccvector_sort(&v, my_compare);

比较器签名与 qsort 一致:

int my_compare(const void *a, const void *b) {
    // 返回负数 → a 排在 b 前面
    // 返回正数 → b 排在 a 前面
    // 返回 0   → 相等
}

qsort 是 C89 标准函数,所有平台(MSVC / GCC / Clang / 嵌入式)均可使用,实现真正的跨平台排序支持。


ccflatmap — 排序数组映射

基于排序数组的 key-value 映射,二分查找 O(log n),插入 O(n)。

使用场景

场景 说明
配置表 启动时批量加载 → push_back + sort O(n log n),运行时仅 find O(log n),不改动
静态字典 编译期确定的键值对(国家代码、语言包),连续内存极低空间开销
只读索引 定期全量重建(push_back + sort),查询 QPS 远高于插入 TPS
二分训练数据 大规模排序数组的二分查找,branchless cmov 免分支预测失败惩罚

数据结构

操作流程

插入:

flowchart TD
    A["二分查找定位插入点"] --> B{"key 已存在?"}
    B -->|是| C["替换 value,❌ 不增加 size"]
    B -->|否| D["插入点右侧元素整体右移"]
    D --> E["写入新元素: key + value"]
    E --> F{"扩容?"}
    F -->|是| G["2× realloc"]
    F -->|否| H["✅ 完成"]

    style C fill:#f96,stroke:#333
    style H fill:#6f6,stroke:#333

二分查找:

flowchart TD
    A["lo=0, hi=len-1"] --> B{"lo <= hi?"}
    B -->|否| C["❌ 未找到,lo 即插入点"]
    B -->|是| D["mid = lo + (hi-lo)/2"]
    D --> E{"data[mid].key == key?"}
    E -->|是| F["✅ 返回索引"]
    E -->|小于| G["lo = mid + 1"]
    E -->|大于| H["hi = mid - 1"]
    G --> B
    H --> B

cctreap — Treap (Tree + Heap)

Treap 是二叉搜索树和堆的随机化结合体。每个节点同时维护 BST 键序(key)和 max-heap 堆序(priority)。随机 priority 使树在期望下保持 O(log n) 高度。

使用场景

场景 说明
排行榜 玩家分数为 key,kth(k) 取第 k 名 O(log n),rank(player) 查排名 O(log n)
分位数统计 kth(size*0.5) 中位数,kth(size*0.99) P99,O(log n) 无需全量排序
滑动窗口 维护最近 N 条记录的有序集合,过期时 erase 最旧,kth 查任意位置
数据库索引模拟 需要 ORDER BY + LIMIT + OFFSET 的单表查询,treap 一条龙支持

数据结构

性质 规则 实现
BST 性质 左子树 key < 当前 key < 右子树 key CCTREAP_COMPARE
堆性质 当前 priority > 所有子节点 priority (max-heap) _TP_PRIO_CMP(内部)

priority 存储在 cctreap_node_t::priority 内,插入时由 xorshift64 自动生成(可通过 CCTREAP_RAND 宏替换)。用户无需手动管理。

操作流程

插入:

BST 下降定位 → 插为叶子 → 向上旋转恢复堆序:

graph TD
    A["BST 插入(设为叶子)"] --> B{"priority > parent.priority ?"}
    B -->|是| C["旋转上浮(左/右旋)"]
    C --> B
    B -->|否| D["✅ 完成,更新 size"]

删除: 将目标节点 向下旋转至叶子 后摘除:

graph TD
    A["目标节点 z"] --> B{"z 是叶子?"}
    B -->|是| C["摘除 z,更新 size"]
    B -->|否| D["比较左右子 priority"]
    D --> E["向 priority 更大的子节点旋转"]
    E --> B

其它特性

利用节点内嵌的 size(子树节点数)实现 O(log n) 确定查询:

kth(第 k 小)

graph TD
    A["x = root, L = size(x.left)"] --> B{"k < L ?"}
    B -->|是| C["x = x.left"]
    B -->|否| D{"k == L ?"}
    D -->|是| E["✅ 返回 x"]
    D -->|否| F["k -= L + 1, x = x.right"]
    C --> A
    F --> A

rank(排名):沿 BST 下降,每往右走一步累加左子树大小 + 1,命中时返回累加值 + size(left)。未找到返回 -1。

kth 和 rank 是确定性 O(log n),不依赖 priority 随机性。

与 ccmap 对比

以下对比中 其它特性 指 kth/rank 等顺序统计操作。

特性 ccmap (红黑树) cctreap (treap)
平衡机制 确定性着色+旋转 随机 priority + 旋转
期望高度 ≤ 2·log₂(n+1) 确定 ≤ O(log n) 期望
最坏高度 2·log₂(n+1) 确定 O(n) 极低概率
节点大小 (64-bit) 24B 32B (含 size + priority)
kth / rank 不支持 O(log n)
迭代 O(log n) / O(1) 均摊 O(log n) / O(1) 均摊
first/last 缓存 ✅ O(1) ✅ O(1)

零开销回调

所有支持比较/哈希的容器均提供两种分发模式:

flowchart LR
    subgraph fn_ptr["函数指针模式 (默认)"]
        A["cmp_fn(a, b)"] --> B["间接调用\n(indirect call)"]
    end
    subgraph macro["宏模式 (零开销)"]
        C["CCXXX_COMPARE(a, b)"] --> D["直接内联\n(zero overhead)"]
    end
    fn_ptr -->|"#define CCXXX_COMPARE"| macro

ccbi — 大数运算

内部表示

小端序符号-绝对值(sign-magnitude),limb 基数为 2³²。

limbs[0..used-1], 小端序
  limb[0] = 低 32-bit, limb[used-1] = 高 32-bit

SSO(Small-String Optimization)

used ≤ CCBI_SSO_LIMBS:  limbs → internal[] (栈上,零分配)
used > CCBI_SSO_LIMBS:  ccbi_grow → malloc → limbs → 堆

加法 / 减法

教科书逐 limb 进位/借位:

for i = 0..max(n,m):
    sum[i] = a[i] + b[i] + carry    (carry = 0/1)

ccbi_limb_add / ccbi_limb_sub / ccbi_limb_mul 是跨容器共用的 limb 原语。

乘法 — 三级派发

n < 16              → schoolbook O(n²)
16 ≤ n < CCBI_TOOM3 → Karatsuba  O(n^1.585)
n ≥ CCBI_TOOM3_THRESH (默认 64) → Toom-3  O(n^1.465)

squaring 自动检测 a == b,交叉项只算一次再×2,减少 ~47% 乘法。

除法 — 左对齐商数位 + 预计算倒数

单 limb 除数快速路径: 教材式逐位试商。

多 limb 除数: 预计算 v_recip = ceil(2^64 / v_top)(一次 divq), 每轮试商用 qd = (u128)u_top * v_recip >> 64(一次 mulq)代替 u_top / v_top(一次 divq)。

while |u| ≥ |v|:
    qd = 试商(mulq)
    u -= qd * v (shifted)
    如果借位传播过头: qd--; u += v
    存储 qd

模幂 — Montgomery CIOS + Sliding Window

预计算 R² mod m
base → Montgomery 域
for 指数 bit 窗口:
    平方 wlen 次
    × table[window](Sliding Window 减少乘法次数)
转换回整数域

位运算

按 magnitude(绝对值)运算,结果恒为非负。

AND:  for i = 0..min(|a|,|b|)-1:  z[i] = a[i] & b[i]  高位截断
OR:   for i = 0..max(|a|,|b|)-1:  z[i] = a[i] | b[i]  高位从较长者拷贝
XOR:  for i = 0..max(|a|,|b|)-1:  z[i] = a[i] ^ b[i]  高位拷贝,可能归零
NOT:  bl = bit_length(a)
      for i = 0..(bl/32)-1:  z[i] = ~a[i]
      if bl%32:  z[last] = (~a[last]) & ((1<<bl%32)-1)  掩码高位

ccbi_not 仅在 bit_length(a) 范围内取反,所以 ~1 = 0~2 = 1

单 bit 操作为 O(1) limb 定位 ± 潜在 grow:

test_bit:  limb = i/32;  (z->limbs[limb] >> i%32) & 1
set_bit:   z->limbs[limb] |= 1 << i%32    (越界自动 grow)
clear_bit: z->limbs[limb] &= ~(1 << i%32) (越界无操作)
flip_bit:  z->limbs[limb] ^= 1 << i%32    (越界等同 set_bit)

字符串转换

from_str — 十进制 9 位分块

普通路径:     N 次 mul_uint(10) + add_uint(digit)
分块路径: ceil(N/9) 次 mul_uint(10⁹) + add_uint(chunk)

非十进制保持逐字符路径。

to_str — 用户缓冲 API

ccbi_to_str_len(z, base)        → 预计算所需缓冲区大小
ccbi_to_str_buf(z, buf, len, base) → 写入用户缓冲区(零分配)
ccbi_to_str(z, base)             → 内部 malloc(向后兼容)

最大公约数 — 二进制 Stein 算法

如果 |a,b| 均为偶数: gcd = 2 * gcd(a/2, b/2)
gcd 逻辑:
    while a != 0:
        while a 为偶数: a /= 2
        while b 为偶数: b /= 2
        if |a| ≥ |b|: a = (a - b) / 2
        else:          b = (b - a) / 2

ccbits — 位运算原语

位运算原语库,跨平台/编译器,GCC/Clang/MSVC 平台使用编译器内建(一条指令), 未知编译器使用纯 C 可移植 fallback。

popcount — 人口计数(Population Count)

SWAR(并行加法器):

将 32/64 位整数划分为不同大小的块,通过并行掩码加法累加 1-bit 计数。

以 32-bit 为例:

Step 1: 每 2-bit 一组统计 1 的个数
  x = x - ((x >> 1) & 0x55555555)

Step 2: 每 4-bit 一组,2-bit 部分和相加
  x = (x & 0x33333333) + ((x >> 2) & 0x33333333)

Step 3: 每 8-bit 一组,4-bit 部分和相加
  x = (x + (x >> 4)) & 0x0F0F0F0F

Step 4: 累加 2 字节
  x = x + (x >> 8)

Step 5: 累加 4 字节,取低 6-bit
  x = x + (x >> 16)
  return x & 0x3F

进位过程图解(以 0x5B = 0101 1011 为例):

                   01  01  10  11
                   ↓   ↓   ↓   ↓
Step 1 (2-bit):    01  01  01  10    (每个 2-bit 块的 1 个数)
Step 2 (4-bit):    0010  0011         (2-bit 和两两相加)
Step 3 (8-bit):    00000101           (4-bit 和相加) → 结果 5 ✓

平台路径:

编译器 实现 指令
GCC/Clang __builtin_popcount{ll} POPCNT (x86) / VCNT (ARM NEON)
MSVC __popcnt16 / __popcnt / __popcnt64 POPCNT
其他 SWAR 5 级加法 纯移位+掩码

clz — 前导零计数

二分分解法:

逐级检测高位区间是否为零,缩小范围。以 32-bit 为例:

n = 32;
if (x >> 16) { n -= 16; x >>= 16; }   // 高 16 位有 1?缩小到高 16 位
if (x >>  8) { n -=  8; x >>=  8; }   // 高 8 位有 1?
if (x >>  4) { n -=  4; x >>=  4; }
if (x >>  2) { n -=  2; x >>=  2; }
if (x >>  1) { n -=  1; }
return n - x;                           // x 最终为 0 或 1,减去 x 即最后修正

图解 clz(0x0A000000)

x  = 0000 1010 0000 0000 0000 0000 0000 0000
    │                                              n=32
    ├─ x>>16 = 0x0A00 ≠ 0 → n=16, x=0x0A00
    │                                              n=16
    ├─ x>>8 = 0x0A ≠ 0 → n=8, x=0x0A
    │                                              n=8
    ├─ x>>4 = 0 ≠ 0? No
    ├─ x>>2 = 2 ≠ 0? → n=6, x=2
    ├─ x>>1 = 1 ≠ 0? → n=5, x=1
    └─ return 5 - 1 = 4  ✓  (前导 4 个 0)

零值安全: 对 GCC/Clang 路径,三元 x ? __builtin_clz(x) : 32 防止 __builtin_clz(0) 的 UB。


ctz — 尾零计数

16-bit:二分分解法(与 clz 类似,从低位开始检测)。

32/64-bit:de Bruijn 乘法(单次乘法 + 查表,无分支):

// 分离最低位: lsb = x & -x          (得到 2^k 形式的幂)
// de Bruijn 序列: 0x077CB531 (32-bit)
// idx = (lsb * 0x077CB531) >> 27    (将 2^k 映射到 [0, 31])
// return table[idx]

原理: de Bruijn 序列 B(2, k) 是长度为 2^k 的循环序列,包含所有 k 位二进制子串恰好一次。 取 k=5 的 de Bruijn 序列 0x077CB531,乘以 2^k(即 lsb)相当于左移 k 位, 高 5-bit 唯一标识 k 的值。只需一个 32 元素查表即可得到 ctz

lsb = x & -x           ← 最低位:0x00000080 (= 2⁷)
lsb * 0x077CB531
   = 0x077CB531 << 7   ← 高 5-bit = 00001 110 → idx = 7
table[7] = 7           ← ctz(0x...80) = 7 ✓

rotl / rotr — 循环移位

所有主流编译器都将 (x << k) | (x >> (N - k)) 模式识别为一条 ROL/ROR 指令。 k = k & (N-1) 保证即使调用方传入 k ≥ N 也不触发 C 语言 UB。

编译器 生成指令
GCC/Clang (x86) ROL / ROR (1 周期)
GCC/Clang (ARM) ROR (单条)
MSVC (x86/x64) _rotl / _rotrROL / ROR

bswap — 字节序反转

16-bit: (x >> 8) | (x << 8)

32-bit: 二级 delta-swap(先交换 16-bit 半字内的字节,再交换半字):

x         = AB CD EF GH
Step 1    = 0B 0D 0F 0H | A0 C0 E0 G0  =  BA DC FE HG
Step 2    = 0000 FE HG  |  BA DC 0000   =  HG FE DC BA

64-bit: 三级 delta-swap(8→16→32-bit)。

编译器 指令
GCC/Clang __builtin_bswap{16,32,64} / MOVBE / REV (ARM)
MSVC _byteswap_ushort/ulong/uint64 / BSWAP

bitrev — 位反转

二进制 delta-swap 法(二分反转):

每次交换相邻 bit 块,块大小逐级加倍,256 次交换后达到完全反转。

以 8-bit 为例:

x        = ab cd ef gh
Step 1   = ba dc fe hg     (交换相邻 1-bit)
Step 2   = dc ba hg fe     (交换相邻 2-bit)
Step 3   = hg fe dc ba     (交换相邻 4-bit)  ✓ 完成反转

32-bit 需要 5 级(1→2→4→8→16-bit),64-bit 需要 6 级。

Clang 特殊路径: __builtin_bitreverse{8,32,64} 在 ARM 上编译为单条 RBIT 指令。


ceilpow2 — 上取整 2 的幂

位涂抹法(无分支):

x--;                      // 处理已经是 2 的幂的情况
x |= x >> 1;              // 将最高位右侧 1-bit 填满
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;             // (32-bit 到此为止)
x |= x >> 32;             // (64-bit 额外一步)
return x + 1;

图解 ceilpow2(21)

x     = 10101 (21)
x--   = 10100 (20)
>>1   = 01010 | 10100 = 11110
>>2   = 00111 | 11110 = 11111
>>4   = 00001 | 11111 = 11111  (已饱和)
...
x+1   = 100000 (32)  ✓

零值处理: x=0x-- 得 0xFFFFFFFF,涂抹后仍为全 1,x+1 溢回 0。


ispow2 — 判 2 的幂

#define ccbits_ispow2_32(x)  (((x) > 0U) & (((x) & ((x) - 1U)) == 0U))

原理: 2 的幂的二进制形式为 100...0,减 1 得 011...1,两者按位与得 0。

4     = 100    → 4-1 = 011    → 100 & 011 = 0  → 是 ✓
3     = 011    → 3-1 = 010    → 011 & 010 = 2  → 否 ✓
0     = 000    → 0-1 = 111    → 000 & 111 = 0  → 0 > 0 为假 → 0 ✓

&(位与)代替 &&(逻辑与)规避短路分支,整个表达式无跳转。


bit_width — 最小位宽

原理: bit_width(x) = floor(log₂(x)) + 1。对于 x>0,等价于 N - clz(x)

bit_width(0)  = 0        (特判)
bit_width(1)  = 32 - 31  = 1
bit_width(8)  = 32 - 28  = 4
bit_width(15) = 32 - 28  = 4    ← 不是对齐到 4,而是 15 需 4-bit 表示
bit_width(16) = 32 - 27  = 5    ← 16 = 10000,需要 5-bit

提供 8/16/32/64 四种宽度,由 clz 派生,无独立平台路径。 窄宽度(bit_width8/16)统一调用 clz32

函数 公式 示例
ccbits_bit_width8 32 - clz32((uint8_t)x) 0xFF → 32-24 = 8
ccbits_bit_width16 32 - clz32((uint16_t)x) 0x8000 → 32-16 = 16
ccbits_bit_width32 32 - clz32(x) 0x7FFFFFFF → 32-1 = 31
ccbits_bit_width64 64 - clz64(x) 0x8000000000000000 → 64-0 = 64

uint8_t/uint16_t 强转先零扩展再 CLZ,编译器优为 movzx + bsr/lzcnt,零额外开销。


mask_low — 低位掩码

原理: (1 << n) - 1。当 n ≥ 宽度 时饱和为全 1(防止 C 语言移位 ≥ 宽度是 UB)。

mask_low(0)  = (1 << 0) - 1 = 0
mask_low(5)  = (1 << 5) - 1 = 0x1F
mask_low(8)  = (1 << 8) - 1 = 0xFF
mask_low(32) → n≥32 → 0xFFFFFFFF

parity — 奇偶校验

原理: popcount(x) & 1。1 的个数为奇数时返回 1,偶数时返回 0。

parity(0x00000000) = 0 → 0 bits → even → 0
parity(0x00000001) = 1 → 1 bit  → odd  → 1
parity(0x00000011) = 2 → 2 bits → even → 0
parity(0xFFFFFFFF) = 0 → 32 bits → even → 0

提供 8/16/32/64 四种宽度,统一由对应 popcount 派生:

函数 实现
ccbits_parity8 popcount8(x) & 1
ccbits_parity16 popcount16(x) & 1
ccbits_parity32 popcount32(x) & 1
ccbits_parity64 popcount64(x) & 1

在 x86 上编译器将 popcount & 1 优化为单条 POPCNT + AND reg, 1


sign_ext — 符号扩展

原理: 将低 N 位视为有符号数,扩展到全宽度。使用经典的 XOR-sub 分支无跳转技巧

m = 1U << (n - 1);           // 符号位掩码
return (int32_t)((x ^ m) - m);

图解 sign_ext32(0x1F, 5)(将低 5-bit 11111 视为 -1 扩展):

x     = ...0 11111           (低 5-bit: 11111, 符号位=1)
m     = ...0 10000           (第 4-bit 掩码)
x ^ m = ...0 01111           (翻转符号位)
-m    = ...1 10000           (补码)

(x ^ m) - m = ...0 01111 - ...1 10000
           = ...0 01111 + ...0 10000    (补码加法)
           = ...1 11111 = -1            ✓

图解 sign_ext32(0x0F, 5)(低 5-bit 01111 = 15,符号位=0):

x     = ...0 01111           (低 5-bit: 01111, 符号位=0)
m     = ...0 10000
x ^ m = ...0 11111           (不变号,仅低 5-bit 翻转)
-m    = ...1 10000

(x ^ m) - m = ...0 11111 - ...1 10000
           = ...0 11111 + ...0 10000
           = ...0 11111 = 15           ✓   (符号位 0 → 正数)

边界处理: n=0 或 n≥宽度 时直接返回 x 本身(无符号扩展空间)。