後端工坊 2026 年 5 月 4 日

2026-05-04 — C3 無號大小失誤、rseq TCMalloc Hyrum 定律、PEP 661 哨兵值

C3 語言的五年設計失誤:從無號到有號的大小型別 c3-la…

C3 語言的五年設計失誤:從無號到有號的大小型別

c3-lang.org · 2026-05-02

C3 語言的設計者在五年後承認,將 usz(size type)設計為無號整數是一個錯誤,並宣布將預設大小型別改為有號整數(signed sz)。這項決定呼應了 Go 和 Java 在設計之初便選擇有號大小型別的策略。

無號大小型別的問題根源

無號整數的語意是模算術(modular arithmetic)而非「非負整數」。當程式碼在有號與無號型別之間混用時,C 和 C++ 標準規定的隱式轉換規則會靜默地產生意想不到的行為:

  • 迴圈條件 i >= 0i 為無號型別時永遠成立,導致無窮迴圈
  • 模除(modulo)在負值操作數上的結果在 C++ 中是實作定義行為,在無號型別下更難推理
  • 環形緩衝區的邊界計算中,head - tailhead < tail 時回繞為巨大的正整數

無號型別的真正用途是:需要模算術語意(如哈希、CRC)、或需要完整的位元寬度(如 u64 旗標位元)。用它來表示「不可能為負的計數或索引」混淆了語意,反而引入錯誤。

Go 與 Java 的先例

Go 的 int 是平台原生的有號整數,其內建的 len()cap() 均回傳有號的 int。Java 完全沒有無號整數型別(直到 Java 8 的 Integer.toUnsignedString 才部分補充)。兩個語言的設計者均有豐富的 C/C++ 背景,刻意迴避了無號大小型別帶來的轉換問題。

C3 的修正方案

新的 sz 型別為有號整數,在 64 位元平台上與 C 的 ptrdiff_t(通常為 int64_t)對應。原有的 usz 保留供需要無號語意的場景使用,但不再作為預設大小型別。API 邊界上的轉換規則也隨之簡化:大部分的陣列索引與長度運算現在都停留在有號域中,只有與 C 互動的介面才需要顯式轉換為 usz

作者總結:「每一次變更都讓程式碼更容易推理,也更正確」——這是典型的「後見之明」設計修訂,承認了歷史慣例(C 的 size_t)對初始決策的不當影響。

原始來源:C3 Blog: Unsigned sizes — a five year mistake


可重啟序列、TCMalloc 與 Hyrum 定律:核心 API 的隱性契約

LWN.net · 2026-04-30

Linux 核心的可重啟序列(restartable sequences,rseq)是 5.1 版引入的機制,允許使用者空間執行不可被搶佔的短序列程式碼,用於無鎖的每 CPU 資料存取。TCMalloc(Google 的記憶體分配器)是 rseq 的主要使用者之一,但它對 rseq 的使用方式與核心文件的規範存在偏差。LWN 的這篇分析展示了這如何演變為核心開發者不得不支援的隱性 API 契約。

rseq 機制簡述

rseq 的運作基於在使用者空間與核心之間共享的 struct rseq。使用者空間程式設計一段「關鍵區段」:

rseq_cs.start_ip = start;
rseq_cs.post_commit_offset = end - start;
rseq_cs.abort_ip = abort_handler;

若核心在關鍵區段執行期間需要搶佔或遷移 CPU,它會將 PC 跳至 abort_ip,讓使用者空間重試。這個機制的前提是關鍵區段必須極短(通常幾條指令),且不能包含系統呼叫。

TCMalloc 的非規範用法

TCMalloc 使用 rseq 來實作每 CPU 快取的無鎖存取。問題在於它在 rseq 關鍵區段結束後,沒有在規範要求的時間點清除 rseq_cs 指標。根據核心文件,這個指標應在關鍵區段結束時立即清零;TCMalloc 延遲了清零,以減少不必要的記憶體寫入。

這個行為在功能上是正確的(實際的競爭條件不存在),但違反了文件規範。當核心開發者試圖在規範基礎上增加新的優化時,便遇到了 TCMalloc 的這個假設。

Hyrum 定律的體現

Hyrum 定律(Hyrum's Law)指出:「當一個 API 有足夠多的使用者時,所有可觀察的行為——無論是否記載於文件——都會被某個使用者依賴。」TCMalloc 正是依賴了 rseq 的一個未記載行為:延遲清零指標不會引發問題。核心開發者於是面臨選擇:破壞 TCMalloc,或將這個行為納入保證。考量到 TCMalloc 在 Google 規模的生產系統中廣泛使用,實質上沒有選擇——未記載的行為成為了事實上的契約。

這個案例與 Linux 核心長期以來的「不破壞使用者空間」政策一脈相承,但也說明了為什麼核心 API 的設計者需要對「觀察到但未明文禁止」的用法保持高度謹慎。

原始來源:LWN: Restartable sequences, TCMalloc, and Hyrum's Law


PEP 661:Python 哨兵值標準歷時五年終獲接受

python.org · 2026-05-01

PEP 661 在提出五年後正式獲得 Python Steering Council 接受,將在 Python 3.15 中引入內建的 sentinel() 工廠函式,為哨兵值(sentinel values)提供標準化的建立方式。

哨兵值的現有問題

哨兵值是程式碼中常見的佔位模式,用於區分「未傳入值」與「傳入 None」:

_MISSING = object()

def f(x=_MISSING):
    if x is _MISSING:
        # 未傳入值
        ...

這個慣用法有三個缺陷:

  • repr 不友善f.__defaults__ 顯示為 (<object object at 0x7f...>,),在函式簽章文件中無意義
  • 型別標注困難:無法為使用哨兵的函式撰寫精確的型別簽章
  • copy / pickle 後失去識別性copy.copy(_MISSING) is _MISSINGFalse,導致 is 比較失效

PEP 661 的解法

新的 API 極為簡潔:

from builtins import sentinel

MISSING = sentinel('MISSING')

建立的哨兵物件具有以下保證:

  • repr(MISSING) 回傳 'MISSING'(即傳入的名稱字串)
  • 可在型別表達式中使用:int | MISSING
  • 透過 copy.copy()pickle.loads(pickle.dumps(MISSING)) 後,is 比較仍為 True
  • 預設為 truthy(鼓勵使用 is 而非布林測試)
  • 只接受一個位置參數(名稱字串),其餘均為關鍵字參數保留

為何花了五年

PEP 661 曾在多個替代方案之間爭論:建立 Sentinel 基礎類別、使用 metaclass、在 enum 模組新增支援、或完全作為第三方函式庫。社群調查顯示 37% 的受訪者支持「專用哨兵工廠納入 stdlib」方案,最終成為被接受的方向。實作本身在 Python 層面很簡單,但 Steering Council 需要確認語義完整性(特別是 pickle 的識別性保證)後才批准納入內建命名空間。

原始來源:PEP 661 – Sentinel Values


End of article
0
Would love your thoughts, please comment.x
()
x