std::uniform_real_distribution的一个bug引发的服务器崩溃

前言

近日发生一次线上游戏服务器宕机问题,通过日志和core文件信息定位到崩溃的函数,但是崩溃的位置却是一段很长时间都没有改动过的代码,起初怀疑是配置数据的问题,但仔细查看之后均正常,然后又怀疑是玩家旧数据异常导致,但是分析代码逻辑后也没发现问题,最后是一个同事发现生成随机数的代码有bug,导致数组越界了,还真是个意想不到的原因。

崩溃问题

崩溃出现在从数组中随机一个数的逻辑中,其中用到了 std::uniform_real_distribution 这个模板类,示例代码如下:

1
2
3
4
5
6
7
8
9
vector<int> v{1, 3, 5, 6};

std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dis(0, 1.0f);

int n = static_cast<int>(dis(gen) * v.size());

return v[n];

之前也了解过 std::uniform_real_distribution<float> dis(0, 1.0f); 这个用法,他应该返回的范围是 [0, 1.0) 内左闭右开的浮点数,所以最终计算出的 n 的值应该为 [0, n-1] 范围内的整数,所以这段代码不应该有问题,但是问题却恰恰出现在 std::uniform_real_distribution 的身上。

std::uniform_real_distribution<> 的bug

std::uniform_real_distribution 这个模板类定义在头文件 <random> 中,是C++11新加的内容,定义如下:

1
2
template< class RealType = double >
class uniform_real_distribution;

可产生均匀分布在区间 [a, b) 上的随机浮点值 x。

但是这个函数有个bug,它有时候会返回边界值b,也就是说实际范围变成了 [a, b]。 可以通过 cppreference.com - uniform_real_distribution查到,具体描如下:

It is difficult to create a distribution over the closed interval [a,b] from this distribution. Using std::nextafter(b, std::numeric_limits::max()) as the second parameter does not always work due to rounding error.

Most existing implementations have a bug where they may occasionally return b (GCC #63176 LLVM #18767 MSVC STL #1074). This was originally only thought to happen when RealType is float and when LWG issue 2524 is present, but it has since been shown that neither is required to trigger the bug.

关于这个bug还可以看一下这个帖子的讨论:

看得时候注意一下这段描述

This problem can also occur with std::uniform_real_distribution; the solution is the same, to specialize the distribution on double and round the result towards negative infinity in float.

bug 重现方法

这个bug有多种变体,其中一个就是说它和 generate_canonical 产生随机数有关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <limits>
#include <random>

int main()
{
std::mt19937 rng;

std::seed_seq sequence{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
rng.seed(sequence);
rng.discard(12 * 629143 + 6);

float random = std::generate_canonical<float,
std::numeric_limits<float>::digits>(rng);

if (random == 1.0f)
{
std::cout << "Bug!\n";
}

return 0;
}

此段代码在编译器 g++ 5.4.0 上编译执行时能重现,但是在 g++ 10.0.3 上已经被修复无法重现了,再看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <random>

int main()
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> dis(0, 1.0f);

while (true)
{
float f = dis(gen);

if (f >= 1.0)
{
std::cout << "BUG\n";
break;
}
}

return 0;
}

这段代码无论是 g++ 5.4.0 版本还是 g++ 10.0.3 都能重现打印出 BUG,这个问题在于模板默认是 double 类型,最后转化成 float 来使用,我按照建议之前帖子中的建议,都改成 double 来使用,之后一直运行了10来天再没出现过随机到边界值的问题。

总结

  • 标准库中的内容很权威,但是不保证一定是正确的,可以持有怀疑态度
  • std::uniform_real_distribution的历史版本是有bug,几乎各个编译器都出现过随机到边界值的情况
  • 这个bug其实在文档中已经指出了,所以大家看文档时还是要仔细一点,往往使用不规范也容易造成bug

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

适当的放松是生活的调味剂,有时候真的需要肆意挥霍一下,一张一弛,生活之道~

2022-8-7 01:30:30

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客