惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
V
V2EX
小众软件
小众软件
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
Recent Announcements
Recent Announcements
有赞技术团队
有赞技术团队
MongoDB | Blog
MongoDB | Blog
C
Check Point Blog
S
Schneier on Security
C
Cybersecurity and Infrastructure Security Agency CISA
The Cloudflare Blog
V
Vulnerabilities – Threatpost
The Hacker News
The Hacker News
T
Threatpost
T
Tenable Blog
aimingoo的专栏
aimingoo的专栏
IT之家
IT之家
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
U
Unit 42
Spread Privacy
Spread Privacy
博客园 - 司徒正美
Hacker News: Ask HN
Hacker News: Ask HN
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
阮一峰的网络日志
阮一峰的网络日志
SecWiki News
SecWiki News
云风的 BLOG
云风的 BLOG
The Register - Security
The Register - Security
AWS News Blog
AWS News Blog
月光博客
月光博客
Security Latest
Security Latest
H
Heimdal Security Blog
S
Secure Thoughts
博客园 - 聂微东
PCI Perspectives
PCI Perspectives
博客园 - 叶小钗
Scott Helme
Scott Helme
O
OpenAI News
Google DeepMind News
Google DeepMind News
Google DeepMind News
Google DeepMind News
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security @ Cisco Blogs
NISL@THU
NISL@THU
S
Securelist
Latest news
Latest news
P
Proofpoint News Feed
博客园 - 【当耐特】

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 Go语言之内存篇 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 又一个类型提升引起的Bug Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 三角形光栅化时遇到的坑 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 HTTP服务器的特点 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
谈谈随机数的使用
重归混沌 · 2020-04-25 · via 重归混沌的BLOG

在日常开发中,伪随机函数几乎是必不可少的一个函数。

大部分我们在使用这个函数时,就自然而然拿来用了,很少去思考用的对不对,反正他是随机的,并且也很难去验证(需要各种大量数据统计)。

所以即使概率看起来不太对,也可以安慰自己说,其实是统计的数据量不够。但有时候真的是因为我们误用了随机函数。

在《计算机程序设计艺术》卷2中,详细介绍了线性同余序列的生成算法。
下面就以线性同余算法为例,来分析一下,为什么随机函数还有可能被误用,他原本不就是随机的么?

在游戏开发中,一般都会设计有开宝箱环节,假设每个宝箱每次开出A的概率是30%,开出B的概率是70%,宝箱可以重复开。

我们的代码可能会这么写

    int open_box(box *b)
    {
        int n = rand() % 1000;

        return  n < 300 ? b->a : b->b;
    }

是的,这段代码就是开宝箱存在“垫刀”的根本原因。

我们来看一下线性同余(LCG)伪随机算法的定义:

Nj+1 = (A*Nj + B) (mod M)(j, j+1为下标)

其中A,B,M为线性同余序列生成常数。

LCG周期为M,A,B,M的关系限定如下:

  1. B,M互质
  2. M的所有质因子都能整除A-1
  3. 若M是4的倍数,则A-1 也是
  4. A, B, N0都比M小
  5. A,B是正整数

通俗点来讲就是,线性同余生成的[0,M)个数在统计学意义上,是等概率出现的。也就是说在足够多次随机以后,他们出现的次数是相同的。

咋一看,感觉上面的代码好像没啥问题。因为[0,M)是等概率出现的,因此rand()%1000之后的值,也是等概率出现的。

但是!我们忽略了一个事实,这段代码意味着。所有人的所有宝箱(甚至还有其他系统)共用了一个伪随机序列。

假设rand()%1000的伪随机序列是这样的:

900,1,300, 500, 299, 785, 556 …

我们来模拟一下多个宝箱交替打开的行为:

开宝箱1,rand()%1000返回的是900, 因此开出来的是B

开宝箱2,rand()%1000返回的是1, 因此开出来的是A

开宝箱1,rand()%1000返回的是300, 因此开出来的是B

开宝箱1,rand()%1000返回的是500, 因此开出来的是B

开宝箱2, rand()%1000返回的是299, 因此开出来的是A

如果宝箱1和宝箱2一直在以类似的顺序交替打开。即使开再多次,你也很难拍着胸脯说,宝箱1和宝箱2开出来的A,B概率分布是符合预期的。

毕竟你亲口告诉玩家,每个宝箱都有30%的概率开出来的是A,但是宝箱1却从来开不出A。

事情之所以会演变成这样。根本原因是,除了有一个伪随机序列之外,还有一个真随机事件,即玩家开宝箱的时机选择。

用软件工程的话来说,宝箱1和宝箱2通过一个全局变量(同一个线性同余序列)耦合在一起了,他们不是正交的。因此,开一个宝箱势必会影响另一个,所以它必然是错的。

还有很多类似的情况,比如一个技能的触发概率。我们本来告诉玩家的是每个技能以某种特定的概率触发,但是我们很可能做成了,以某种概率释放了某个技能。

在我们用随机函数之前,一定要先问问自己,所有使用rand()函数的地方其实是共用了同一个伪随机序列,这样真的没问题么?


2021/11/16补充:

经过这一年多来的观察, 除了"垫刀"的问题之外, 我们还习惯使用如1000, 10000之类的数字来代替100%的概率. 这就会产生一个现象, 就是1000或10000很可能和LCG公式中的M是有公约数的, 这会导致LCG的产生的随机数分布不均匀, 其影响远大于"垫刀"产生的不良影响。在我们将接近1000或10000的质数作为100%概率的代表时,随机数的分布有明显改善。


2025/06/05补充:

现代的随机数算法接口通常返回的是 [0, 1) 区间内的浮点数,而不是像标准 C 那样返回一个整数值。这种设计可以有效避免因公约数带来的不均匀分布问题。

因为返回的是 [0, 1) 之间均匀分布的数值,当我们将其按比例缩放到目标范围时,结果也会保持均匀性。