((float) rand() / (float)((1 << 31) - 1)) 的含义是什么?

6

我正在尝试理解一个包含以下代码行的C程序中的.h文件:

#define random                  ((float) rand() / (float)((1 << 31) - 1))

该C程序还包括<math.h>库。
我的猜测是它只会从区间[0,1]上的均匀分布中产生一个随机数,这正确吗?

在所有最多只有32位的int平台上,会出现未定义行为。任何事情都可能发生,案子已经结案 - 请下一个! - too honest for this site
1
可能是[当sizeof(int)== 4时,C中1 << 31是否定义良好]的重复问题。 - Bo Persson
3个回答

10
表面上是可以的,但它有两个主要问题:
  1. 使用 RAND_MAX。那是为什么存在的。它可能比 1 << 31 - 1 要小得多。

  2. 1 << 31 在具有 32 位 int 或更少的平台上会产生不确定行为,这是非常普遍的情况。不要这样做!

请注意,如果您不想恢复值1(通常情况下),则在分母上使用 RAND_MAX + 1.01.0 强制执行浮点运算:如果您写入 RAND_MAX + 1,则可能会溢出整数类型。


在具有16位以上但少于33位的平台上,它还会调用UB。而且,在所有平台上,“RAND_MAX + 1”很可能会调用UB。 - too honest for this site
如果您不介意,我已经编辑了答案。两个观点都非常好。谢谢。 - Fitzwilliam Bennet-Darcy
但是 RAND_MAX + 1.0 不是 double 类型吗? - Fitzwilliam Bennet-Darcy
嗯,说得好。我不在我的应用程序中使用浮点数(在嵌入式系统上通常是一个非常糟糕的想法)。你是对的。如果忽略潜在的性能问题,可以使用它。但标准随机函数并不保证算法的任何质量。如果需要真正的均匀分布,则应仔细选择算法,并且如果确实需要浮点范围,则应直接使用它。 - too honest for this site

5
rand函数返回一个0到RAND_MAX之间的值。这个程序假设RAND_MAX是2^31 - 1,然后将结果除以该数字。

所以,如果上述假设成立,则此宏会给出一个[0,1]之间的数字。它不是均匀随机分布,而是“伪随机”值。

至少它应该做到这一点。这个表达式(1 << 31)会引发未定义行为(假设int为32位或更小),因为常量1的类型是int,左移31位使其超出了int的范围。实际上,如果使用二进制补码表示法,则一些编译器将允许进行这种移位,然后后续的-1将把它放回范围内,但不能依赖于此。

可以通过使用(1U << 31)来避免这种未定义行为,这样常量1就具有unsigned int类型,以便移位在范围内。更好的选择是忘记移位和减法,直接使用0x7fffffff

但是为了最大限度地提高可移植性,应该定义如下:

#define random ((float)rand() / RAND_MAX)

但是仍然存在一个问题。一个浮点数通常有23位的尾数。如果rand返回32位值,你将得不到良好的数字分布。最好使用double,它有52位的尾数:

#define random ((double)rand() / RAND_MAX)

你没有看到任何未定义行为吗? - too honest for this site
@Olaf 对,对此进行了详细说明。 - dbush
那个移位操作有问题 - 最好情况下也是如此。在少于32位的实现中,它仍会调用未定义行为(移位计数>=位宽)。使用RAND_MAX方法更好。但是使用float很可能会失去精度,并且通常将分布限制为不超过23位(IEEE float的尾数)。 - too honest for this site

1
我的猜测是,这只是在[0,1]区间上从均匀分布中产生一个随机数;这正确吗?
不对。代码可能永远不会返回1.0f,也不会得到均匀的结果。
#define random ((float) rand() / (float)((1 << 31) - 1)) 存在很多问题。这是弱代码。
精度丢失:典型的float大约有24位精度。如果将rand()的结果转换为超过24位,将得到一个float,由于四舍五入而可能与原始值不同。这会削弱/破坏随机数生成的均匀性。不同的rand()结果将产生相同的答案。另请参见@Olaf 解决此问题很棘手,因为OP显然想要从集合[0,1/2,147,483,648,2/2,147,483,648,... 2,147,483,647/2,147,483,648]中获得均匀分布的随机数,但考虑到float的可能精度限制,这是不可能的。

最糟糕的是 (1 << 31) 是未定义行为 UB,除非 int 至少有33位。将1进行移位操作到符号位上是UB。C11dr §6.5.7 4.

为了避免 UB,使用 ((1ul << 31) - 1)

然而,使用神奇数字 ((1ul << 31) - 1) 比基于 RAND_MAX 的分数更不可靠。

进一步地,(float) ((1ul << 31) - 1) 可能会因为如上所述产生精度损失,它形成的值是 2147483648.0f 而不是不可获取的 2147483647.0f 。OP的代码可能永远也无法生成 1.0f


我猜测OP实际需要的是[0..1)的结果,而不是[0..1]。两者都在下面。
// generate a `double` in the range [0 ... 1)
#define random0to_almost1  (rand() / (RAND_MAX + 1.0))
// or 
// generate a `double` in the range [0 ... 1]
#define random0to1  (rand() / (RAND_MAX + 0.0))

请注意,如果double(通常为53位)的精度超过RAND_MAX的需求,则会像OP的原始代码一样遇到问题。
为了应对这种情况,一个缓解措施是确保RAND_MAX + 1.0被准确执行。在极其常见但不是C指定的情况下,RAND_MAX是2的幂减1。因此,RAND_MAX/2 + 1是一个int和一个精确的2的幂。将该int转换为double肯定是精确的。
#define random0to_almost1  (rand() / (2.0*(RAND_MAX/2 + 1)))

一种浮动的解决方案是:
// This value is platform dependent, but very common
// Do not a a highly portable generation method yet.
#define FLT_POWER2_INTEGER_LIMIT (1ul << 24)


#define random0to_almost1  ( \
    (rand() % FLT_POWER2_INTEGER_LIMIT) /  \
    (RAND_MAX % FLT_POWER2_INTEGER_LIMIT + 1.0f) \
    )

1
如果FLT_RADIX不是2的幂,则将2的幂int值转换为double可能不精确。 - Ian Abbott
@Ian Abbott 是的-同意。C允许FLT_RADIX2,3,4,5...。我还没有遇到过除了2,10,16之外的FLT_RADIX-当然未来可能会有所不同。当然,使用2,4,8,16,...等值并不是以上答案的问题。对于基数为10,使用上述方法仍然有好处,但可能无法达到_精确性_。如果您知道今天使用的任何非基数2系统,我很想听听它们的情况。 - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接