为啥从内存中插入的“随机”数字通常非常大?

Posted

技术标签:

【中文标题】为啥从内存中插入的“随机”数字通常非常大?【英文标题】:Why are "random" numbers plugged from memory often extremely large?为什么从内存中插入的“随机”数字通常非常大? 【发布时间】:2019-12-11 14:15:26 【问题描述】:

有时在开发过程中,例如在 C 代码中,您可能会意外地索引超出其最后一个元素的数组,从而导致读取本质上“随机”的内存块。我经常使用doubles 的数组,并注意到当发生这种情况时,从“随机”内存产生的double 通常非常大,例如大于1e+300。我想知道这是为什么。

如果用于解释 double 的 64 位是真正随机的,我希望 double 的指数从 0 均匀分布到 308(忽略指数的符号),由于使用科学(指数)表示法在内存中排列浮点数的方式。当然,内存中随机选择的位的值本身并不是随机分布的,而是对应于任何进程设置这些值的一些有意义的状态。

为了研究这种影响,我编写了以下 Python 3 脚本,该脚本绘制了从“随机”但未使用的内存中提取的真正随机生成的 doubles 和 doubles 的分布:

import random, struct
import numpy as np
import matplotlib.pyplot as plt

N = 10000

def random_floats(N=1):
    return np.array(struct.unpack('d'*N, bytes(random.randrange(256) for _ in range(8*N))))

def exp_hist(a, label=None):
    a = a[~np.isnan(a)]
    a = a[~np.isinf(a)]
    a = a[a != 0]
    if len(a) == 0:
        print('Zeros only!')
        return
    a = np.abs(np.log10(np.abs(a)))
    plt.hist(a, range=(0, 350), density=True, alpha=0.8, label=label)

# Floats generated from uniformly random bits
a = random_floats(N)
exp_hist(a, 'random')

# Floats generated from memory content
a = np.empty(N)
exp_hist(a, 'memory')

plt.xlabel('exponent')
plt.legend()
plt.savefig('plot.png')

运行此脚本的典型结果如下所示:

真正随机生成的doubles 的指数确实是均匀分布的。

从内存内容解释的doubles 的指数要么非常小,要么非常大。事实上,大部分未使用的内存都被清零了,从而产生了很多 0 值,这是有道理的。然而,正如我经常从跳出内存访问中体验到的那样,1e+300 附近的许多值也会出现。

我想解释一下这么大数量的超大doubles。

脚本运行注意事项

如果您想亲自试用该脚本,请注意您可能需要多次运行它才能显示任何有趣的内容。从内存内容中读取的每个数字都可能是0,在这种情况下它会告诉你。如果这种情况反复发生,请尝试降低N(使用的doubles 的数量)。

【问题讨论】:

“随机”实际上是指统一。均匀(随机)分布是其中每个元素具有相等的发生概率的分布(或者,对于连续分布,每个区间的发生概率与区间的大小成正比)。不同元素具有不同概率的分布仍然是随机的,只是不均匀。 内存布局倾向于将地址空间中的东西聚集在 0、0x7fff…、0x8000…和 0xffff…附近。所以值在 0x7ff 左右的指针的存在可以解释大的 double 值。 @EricPostpischil 当这是预期的含义时,该问题专门使用了术语均匀随机。为什么内存布局倾向于围绕这些值进行聚类? “如果……是真正随机的”,应该是“真正随机且均匀分布的”。 “真正随机生成”应该是“均匀分布的真正随机生成”。 内存布局围绕着这些价值观聚集,因为很久以前人们会坐在那里思考“我应该把东西放在哪里?”他们中的一些人从 0 开始,然后将下一件事放在 1,然后是 2,依此类推,或者是 4、8、12,或者他们需要的任何倍数。然后有人说好吧,我们已经把我们的代码和我们的一些数据放在那里了。嘿,我有个主意,让我们做一个堆栈。我们应该把它放在哪里?由于“底部”(0)已经被使用过,也许它们从地址空间的顶部开始(可能是当时的 0xffff)并向下工作。 【参考方案1】:

您可能会在内存中找到许多不同的东西,但其中有很多会映射到非常大或非常小的浮点数、无穷大或 NaN。在下文中,“FP”表示 IEEE 754 64 位二进制浮点数。

首先,因为他们已经在 cmets 中讨论过这个问题,所以请考虑地址。 64 位地址的所有指数位通常为零(内存的低端),或所有指数位的高位(内存的高端,通常是堆栈地址)。如果所有指数位都为高,则它是无穷大或 NaN,程序似乎忽略了它。如果所有指数位都为零,则它是次正规数或零。次正规数均小于2.3E-308,计为指数308。

现在考虑 32 位整数,这是另一种非常常见的数据形式。映射到有限 FP 的负 32 位二进制补码整数为 -1048577 或更小。像 -42 或 -1 这样的数字映射到 NaN,被程序忽略。类似地,中等值正整数的所有指数位为零,因此映射到次正规数,映射到直方图的大指数端。即使是小的正态数也对应于惊人的大整数。例如,1e-300 的前 32 位具有整数值 27,618,847。

对于指针和整数,对所有具有相同值的指数位都有强烈的偏见,要么全为零,要么全为一。所有一个都是 NaN 或无穷大,程序不计算在内。所有的零都是次正规的,算作非常大的指数。

【讨论】:

以上是关于为啥从内存中插入的“随机”数字通常非常大?的主要内容,如果未能解决你的问题,请参考以下文章

如何随机打乱 bigquery 数据集中大表的所有行?

需求:lr需要在一串数字中随机位置插入一个新数字的实现方式

使用并行训练带有插入符号的随机森林

为啥当我将数字添加到字符串时,它会在 C++ 中显示随机文本?

从数组中选择一个随机值

MySQL:为啥将主键与不​​使用索引的随机生成的数字进行比较?