雜記 › 技術筆記 › 60 位時間戳在這一頁只剩 28 個位置:UUID 產生器 v1 分支的移位與掩碼逐毫秒實測

60 位時間戳在這一頁只剩 28 個位置:UUID 產生器 v1 分支的移位與掩碼逐毫秒實測

這一頁 v1 只把時間戳讀進一個 32 位元窗口:移位計數按 32 取模,time_mid 與 time_hi 成了低位重讀,讀到 28 個位置、會變的只剩 24 個;負 leg 下十六進位只剩 31 位。

這一頁的 v1 到底讀了時間戳的哪幾位

這一頁 v1 的時間戳段看著像 60 位,但逐毫秒跑下來,真正讀到的只是一個 32 位元窗口。generateV1 在第 126 到 162 行,第 131 行算出 ts = (now - 1582 紀元) * 10000,第 134、135、136 行分別取 ts & 0xFFFFFFFF、(ts >>> 32) & 0xFFFF、(ts >>> 48) & 0x0FFF。只針對本頁、不外推到別的實作。

移位計數取模 32:60 位塌成 28 個位置、24 個會變的位元

>>> 的移位計數按 32 取模,於是 >>> 32 就是 >>> 0、>>> 48 就是 >>> 16。20000 個連續毫秒上 timeMid === (timeLow & 0xFFFF) 與 timeHi === ((timeLow >>> 16) & 0x0FFF) 都是 20000/20000:time_mid 是 time_low 低 16 位的重讀,time_hi_and_version 低 12 位是同一窗口第 16 到 27 位的重讀。三個欄位讀到的位合起來只有位元 0 到 27 共 28 個,位元 32 以上沒進代碼。

會變的位比 28 還少:以 2000000 個毫秒步長掃描,按位或是 0xfffffff0、按位與是 0,恆 0 的是位元 0 到 3;與被讀到的 28 個位置取交集,真正會變的只剩 24 個位元。第 138 行 timeHi | 0x1000 又把版本位釘成恆 1。

時間戳只落到一個 32 位元窗口上 位元 0..31 = time_low;讀到 位元 0..27 共 28 個 恆 0 位元 0..3;真正會變的位元 = 24 OR = 0xfffffff0、AND = 0(2000000 毫秒掃描) timeMid = timeLow & 0xFFFF:20000/20000 timeMid 與 timeHi 各只有 4096 個不同值

圖 1:三處取值都在同一窗口上重讀

恆 0 的末 nibble 落在第 8 個字元

時間戳恆為 16 的倍數:20000 個毫秒上 ts % 16 === 0 成立 20000/20000,而 ts % 32 的 0 與 16 各佔 10000——「32 的倍數」只在半數毫秒成立。落到串上 time_low 段末位即第 8 個字元恆「0」,20000 取樣與 100 條批次全中;反倒是最後一個字元(節點段末 nibble)在 16 個值上分佈、取「0」只有 3/100。

timeLow 走 ToInt32:正負兩條 leg 按毫秒翻轉

timeLow = ts & 0xFFFFFFFF 是有符號 Int32,相鄰毫秒裡既給正也給負。整週期 = 2^32 / 10000 = 429496.7296 ms 即 7.1583 分鐘,負值段 = 2^31 / 10000 = 214748.3648 ms 即 3.5791 分鐘。沒有打樁的真實時鐘第一次翻轉只要 109614 ms;2400000 毫秒掃描負值佔 1242928 步 = 51.79%,「按年才會碰到負值」只是抽樣造成的錯覺。

timeLow 走 ToInt32:正負兩條 leg 整週期 429496.7296 ms = 7.1583 分鐘 負值段 214748.3648 ms = 3.5791 分鐘 深負 -68b10840:36 碼元、十六進位只剩 31 個 淺負 -1b80:padStart 把 0 補在減號之前 關掉連字號才露出真實串長 33 或 32 碼元

圖 2:負 leg 的週期與串形

負 leg 不改長度只改內容:深負 -68b10840 的減號下標 [0,8,13,18,23],淺負 -1b80 因第 153 行 padStart(8,'0') 把 0 補在減號前、減號挪到下標 3。第 98 行 substr(20,12) 只消費前 32 個字元、丟掉節點段末 nibble,十六進位只剩 31 個;1000 行批次長度分佈 {36:1000}、每行減號個數 {5:1000}。

捨入的邊界在 2^57,不在 2^53

當下 ts 已超 2^53 約 15.56 倍、落在 2^56 與 2^57 之間,這檔 ULP 正好是 16;而 diff × 10000 恆為 16 的倍數(10000 = 16×625),乘積本就落在網格上,當下 1000 毫秒捨入 0 條。真正開始捨入要等 ts 跨過 2^57、ULP 變 32:樁到 2039-06-20T23:40:07.585Z 起 1000 毫秒捨入 499 條、2045-01-01 檔 500 條;2^53 對應 1611 年,本頁走不到那個時刻。

輸入真值表:數量框、開關與兩枚掩碼

輸入項實測口徑結果
數量框 17 種輸入三級兜底:先轉整數,再 || 1,最後夾到 1 到 1000、空字串、"abc" 各 1 行回寫 1;101、1000、1000000 各 100 行回寫 100;"2.7" 截成 2 行
三枚開關 8 種組合長度只由連字號與大括號決定,大小寫不改長度32 / 34(加大括號)/ 36(加連字號)/ 38(兩者都加)碼元四種
v4 兩枚掩碼第 115、116 行第 15 字元恆「4」100/100;第 20 字元落在 8/9/a/b,分佈 {8:26, 9:23, a:29, b:22}
nodeBytes[0] | 0x01第 149 行置首個位元組的最低位落在節點段第 2 字元全為奇數 1000/1000;第 1 字元只有 517/1000 為奇數

批次與清場

樁在同一毫秒、count=100 時,v1 的 100 行前 14 個字元 distinct=1,差異全來自 clock_seq 的 4 字元與節點段 12 字元。真實時鐘跑 1000 次 generateV1() 跨 14 ms,time_low 段 distinct=13、clock_seq 段 distinct=966、整串 1000。清場有殘留:清空 lastUuids 後 renderList() 只把結果卡的 display 寫成 none、統計格歸 0,但結果容器 innerHTML 仍留 1285 碼元。

對照列表頁文案

對照 tools.php 的 intro 共 25 條判定:一致 11、衝突 4、部分一致 10。最要緊的衝突是那句「8-4-4-4-12 共 32 個十六進位字元」:對齊批次是 32 位,本頁負 leg 只剩 31 位;另一處是 keywords 寫著 GUID,而模板全文沒有這個詞。「v1 由時間戳和節點資訊生成」屬部分一致:本頁節點段是每次重搖的隨機位元組,沒讀網路卡。版本位與變體位見舊文 UUID 產生器舊文,不復述它那套場景與機率;本頁隨機段那條介面與隨機密碼產生同族,熵源品質未實測;細節以 UUID 產生器工具頁 為準。

FAQ

Q:這一頁的 v1 時間戳段是不是標準的 60 位?

不是。三個欄位只在這一個 32 位元窗口取值,讀到的位只有位元 0 到 27 共 28 個、扣掉恆 0 的最低 4 位後只剩 24 個會變的位元。

Q:為什麼有時 uuid 裡冒出一個減號、還少一位十六進位?

因為 timeLow 走 ToInt32、在近半數毫秒落在負 leg;substr(20,12) 又截掉節點段末 nibble,十六進位只剩 31 個。

📎 相關文章

取色器的解析門只收六位十六進位:三位短寫摘出能算、頁面永遠走不到 2026-11-08 改了鹽值不點按鈕,四個框一動不動:MD5 頁的三段拼接與多行輸入實測 2026-11-08 替換次數報 5 還是報 1:批次尋找替換的三個開關與零寬匹配實測 2026-11-07 空一個框就 0 張卡:分數通分頁的靜默失敗、跳號的「分數N」與 990 行不約分 2026-11-07 刪右側給空串、截右側還整行:text-line-ops 的碼元口徑與越界不對稱實測 2026-11-07