杂记 › 技术笔记 › 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 个。

📎 相关文章

取色器只收六位数:三位短写摘出能算、页面永远走不到,全空间恒等率 10.5813% 2026-11-08 改了盐值不点按钮,四个框一动不动:MD5 页的三段拼接与多行输入实测 2026-11-08 替换次数报 5 还是报 1:批量查找替换的三个开关与零宽匹配实测 2026-11-07 空一个框就 0 张卡:分数通分页的静默失败、跳号的「分数N」与 990 行不约分 2026-11-07 删右侧给空串、截右侧还整行:text-line-ops 的码元口径与越界不对称实测 2026-11-07