有人在推特上问了微软工程师Raymond Chen一个很冷的问题:Windows XP第一次建账户时,那个随机弹出来的小头像,到底是怎么选的?Chen在他运营了二十多年的博客Old New Thing上认真回了一篇。答案不复杂,但顺着往下挖,会发现网上流传的“复现版算法”其实是错的,而很多人抱怨的“头像总重复”跟算法本身没什么关系。

一遍扫完,不用数总数

Chen给出的答案是:系统调用RtlRandomEx,种子是当时的GetTickCount()(开机以来的毫秒数),从Default Pictures目录里做一遍水塘抽样(reservoir sampling,k=1),最多采样100张就停。

这套算法的巧妙之处在于不用先数清楚目录里到底有多少张图。朴素做法是先遍历一遍数总数,再随机挑个序号,再遍历第二遍找到那张图——两次磁盘I/O。水塘抽样只走一遍:遇到第一张先记下来当候选,遇到第n张时以1/n的概率把候选换成它,遍历结束候选就是最终结果。

一遍水塘抽样怎么做到公平 输入 目录里的图片 数量未知 处理 逐张遍历一次 第n张以1/n概率 替换当前候选 最多采样100张 输出 等概率随机 选中1张

这不是XP独有的发明,是计算机科学里的经典技巧,本质是k=1的水塘抽样特例。好处很实际:减少文件系统调用(当年真正的性能瓶颈),也不怕目录内容在遍历过程中被别的进程改动。100张的上限是安全阀,防止有人在目录里塞进海量文件拖慢开机,不是XP出厂头像的实际数量——网上不少地方把这个数字当成“默认头像总数”引用,是把两件事混为一谈了。

种子选得太随便,装机流水线上会撞脸

算法设计得干净,但种子选得糟糕。GetTickCount()返回的是开机后经过的毫秒数,精度粗、可预测性高,这在当年是常见的“廉价”随机源,图省事。

问题出在批量场景。企业IT装机、克隆镜像这类操作,往往是同一套流程在极短时间窗口内连续跑,多台机器开机后的tick值可能非常接近,甚至撞在同一个毫秒。种子一样,RtlRandomEx吐出来的序列大概率也一样,选中的头像自然就重复了。这不是水塘抽样算法出了问题,是喂给它的随机源本身不够随机。这一层Chen的原文完全没提,却恰恰是这套设计在真实部署环境里最容易翻车的地方。

  • 风险.批量装机场景下,弱种子导致的头像撞脸,比算法公平性问题更值得工程师警惕。

网上的“复现代码”,其实是另一套逻辑

搜一下“XP头像随机算法怎么实现”,会看到不少复现版本,思路是先把目录里所有文件列出来排序,再用随机数结果对文件总数取模(seed % count)选一张。这是典型的两遍式做法,跟Chen描述的一遍水塘抽样是两套完全不同的工程方案——前者要先知道总数,后者压根不需要。

这类以讹传讹的版本能跑通、能选出一张图,看起来“正确”,但和微软工程师给出的一手说明并不是同一件事。技术类内容传播里,“代码能跑”和“代码就是真实实现”经常被划等号,这个案例是个不错的提醒。

你觉得总重复,其实是概率在骗你的直觉

抛开种子问题,就算随机源完全干净,小样本下看到重复头像也很正常。假设默认图片是二十张左右这个量级,两个账户随机撞上同一张的概率大约4.76%,五个账户里出现至少一次重复的概率能到40.2%,六个账户则超过一半,约54.4%

21张图里,头像撞车的概率 2个账户 4.76% 5个账户 40.2% 6个账户 54.4%

这其实是生日悖论的另一个版本——房间里凑够二十几个人,两人同一天生日的概率就高到反常识。用户觉得“怎么又是这张猫”,多半是这个数学现象在起作用,跟RNG是否公平没有必然关系。

头像重复不是bug,是生日悖论在提醒你算概率别靠直觉。
  • 结论.一遍水塘抽样在今天的流式数据处理、大规模A/B测试抽样里依然是标准手法,XP这个二十年前的小设计,反而是个不错的教学案例。

Chen这类博客的价值就在这里:不是挖出惊天秘密,是把一个被随手写完、从没人细看的决策拆开,让人看清工程师当年在效率、鲁棒性和“够用就行”之间做的取舍。种子选得糙,是那个时代的通病,放在今天任何一个还在用系统时钟当随机源的遗留脚本里,问题都原样存在。