将随机整数转换为范围 [min,max] 而不进行分支
Posted
技术标签:
【中文标题】将随机整数转换为范围 [min,max] 而不进行分支【英文标题】:Transform random integers into range [min,max] without branching 【发布时间】:2021-01-04 10:57:55 【问题描述】:我得到了hold on an SUPER-FAST algorithm,它统一生成一个随机字节数组。比标准库的 c++ 均匀分布和 mersenne-twister 快 6 倍。
数组的计数可以被 4 整除,因此可以解释为整数数组。将每个条目转换为整数,会产生[INT_MIN, INT_MAX]
范围内的值。但是如何将这些整数值转换为介于我自己的 [min, maximum]
之间?
我想避免任何 if-else,以避免分支。
也许我应该应用一些按位逻辑来丢弃每个数字中不相关的位? (因为所有剩余的未屏蔽位无论如何都是 0 或 1)。如果我可以提取最大值中的最高有效位,我可以在我的整数中屏蔽任何比该位更重要的位。
例如,如果我希望我的 max
为 17,那么它是二进制形式的 00010001
。也许我的面具会看起来像00011111
?然后我可以将其应用于数组中的所有数字。
但是,这个掩码是错误的......它实际上允许值高达(1+2+4+8+16)
:(
我能做什么?还有,min
怎么保养?
编辑
我的应用程序的每一帧都为神经网络生成数百万个数字。我设法使用 AXV2 对浮点变量的代码进行矢量化(使用 this post),但也需要让整数工作。
【问题讨论】:
您使用的是哪个 C++ 版本?值是否保证为正? 既然你用的是C++,why not using C++? 如果min
和 max
不是 2 的幂,我猜位掩码没用。
在[min, max]
中生成一个随机数,相当于在[0,max-min]
中生成一个随机数,然后加上min
。这减少了在[0,max]
中生成数字的问题。如果max
的形式为2^n-1
,那么您可以切断多余的位。但如果不是,则没有算法可以在[0,max]
中从01
统一生成器生成随机数,这样结果是统一的并且它具有停止属性。还要忘记避免分支。
典型的解决方案是rand() % max
,这是不统一的。另一个是do result = rand(); while (result > max)
,它是统一的,但没有停止属性。如果我没记错的话,即使预期的迭代次数大约是 6。
【参考方案1】:
但是如何将这些整数值转换为介于我自己的
[min, maximum]
之间?
由于范围可能不是 2 的幂,因此位掩码已失效,但您已经发现了。
Modulo 也被淘汰了,它在 AVX2 中不作为原生操作存在(即使有,也不一定能提高效率)。
还有另一种选择:乘高,使用_mm256_mul_epu32
(不幸的是,对于 32 位数字没有“纯”乘高,就像 16 位数字一样,所以我们被困在一个只能做的操作50% 有用的工作)。这里的想法是取输入数字x
(全范围)和所需范围r
,然后计算r * x / 2^32
,其中除法是隐式的(通过取乘积的高半部分来实现)。
x / 2^32
将是 [0.0 .. 1.0) 中的数字(不包括 1.0),如果它被解释为有理数,乘以 r
然后将范围扩展到 [0.0 .. r
) (不包括r
)。这不是它的计算方式,而是公式的来源。
通过将min
添加到缩放结果,可以轻松设置范围的最小值。
在代码中(稍作测试):
__m256i squish(__m256i x, int min, int max)
__m256i sizeOfRange = _mm256_set1_epi32((unsigned)max - min);
__m256i scaled_even = _mm256_shuffle_epi32(_mm256_mul_epu32(x, sizeOfRange), 0xB1);
__m256i scaled_odd = _mm256_mul_epu32(_mm256_shuffle_epi32(x, 0xB1), sizeOfRange);
__m256i scaled = _mm256_blend_epi32(scaled_even, scaled_odd, 0xAA);
return _mm256_add_epi32(scaled, _mm256_set1_epi32(min));
它仍然是一个独占范围,它不能处理完整的[INT_MIN .. INT_MAX]
作为输出范围。甚至无法指定它,它最多可以做的是[INT_MIN .. INT_MAX)
(或例如具有零偏移的等效范围:[0 .. -1)
)。
它也不是真的统一的,出于同样的原因,简单的基于模的范围缩小并不是真正统一的,你不能公平地将N
弹珠划分为K
垃圾箱,除非@ 987654337@ 恰好平分N
。
【讨论】:
谢谢!为了再次向未来的用户强调这一点,这会产生不包括 [min, max) max 范围内的数字。而且,此代码默认也适用于unsigned int
:)
我们是否需要 sizeofRange 行中的(无符号)强制转换?我删除了它,它没有工作,包括负整数
@Kari 这并不重要,但无论如何它应该是免费的。我只是把它放在那里以防止任何签名的溢出恶作剧【参考方案2】:
核心思想是使用模而不是位掩码,这在非 2 次幂的情况下是无用的。没有分支也是一个有点奇怪的要求。你想要的是“足够快”,而不是“没有分支和按位掩码”。
所以假设我们有一个函数
int rand();
均匀地产生一个随机整数。如果max
的形式为2^n-1
,则如下
rand() % (max+1)
将统一生成[0,max]
范围内的随机整数。那是因为整数的总数是 2 的幂。
现在如果min
和max
是这样的max-min
是2^n-1
的形式,那么以下
(rand() % (max-min+1)) + min
将统一产生[min, max]
范围内的随机整数。
但是当max-min
不是2^n-1
的形式时会发生什么?那么我们就不走运了。 (rand() % (max-min+1)) + min
方法仍然会在[min, max]
范围内产生一个随机整数,但不再均匀。这是为什么?因为当n
是固定的而不是2 的幂时,给出具体r = x % n
结果的整数总数取决于r
。
不过方法还不错。 max-min
值越大,它越接近均匀分布,并且在实践中通常已经足够好。而且速度非常快,没有分支。
另一个例子是
upper = get_upper_power_of_2(max - min)
do
tmp = rand() % upper;
while (tmp > max - min);
result = tmp + min;
这个方法有一个很好的属性,它是统一的,但它没有停止属性,即理论上这个算法可能永远不会停止。它也有分支。但在实践中,它确实停止得非常快(很有可能),因此它是一种非常常见的算法。例如,它位于标准 Java 库中。
当max-min
溢出时(即min
是一个很大的负数),这两种方法当然都会出现问题,如果我们切换到无符号整数然后再返回整数,可以解决这个问题。
据我所知,当max
不是来自01
统一生成器的2^n-1
形式时,没有算法可以在[0, max]
中生成随机整数,这样结果是统一的并且它具有停止属性.我认为不存在这样的算法,但我没能在计算机科学中找到合适的结果。
【讨论】:
所以你不把do
/while
循环算作分支?
@Caleb 我当然愿意。也许我应该明确地说出来。固定。
我的错 — 我将 它非常快,没有分支 与紧随其后的代码混为一谈,但您明确表示这是一个不同的解决方案。对不起!不过,我认为您的编辑是一种改进。
我让 OP 放松了位掩码的想法。我相信您的回答是出于其他(和适当的)原因讨论两个的权力。因此,这个问题并没有成为您回答的目标。否则请接受我的道歉。
这是矢量化的。此处是 AVX2 的草图:godbolt.org/z/hrjrda。对于 AVX-512,可以使用 _mm_ternarylogic_epi32
进行更快的混合。其他一些可能值得尝试的事情是:1. 展开前 N 次迭代的循环,2. 与其只替换不在我们浪费 rng 循环的范围内的值,不如生成 8值并以连续方式存储k<8
,我认为这可以通过屏蔽稀疏存储或随机+存储来实现。方法 2. 也会使其“无分支”【参考方案3】:
如果一个值中有 2^N 个随机位,则可以通过以下方式将其放入整数范围:
r = ((value * (max-min)) >> N) + min
实际上,您将您的值视为乘法的分数。 你一定会得到 `[min...max)' 中的值
这最终是两个可向量化的操作:mulhi
, 'add'
r = _mm256_add_epi16(
_mm256_mulhi_epi16(value, _mm256_set1_epi16(max-min)),
_mm256_set1_epi16(min));
虽然如果你想要 32 位,看起来你需要两个 mul_epi32
和一个随机播放来获得你的结果。
对于 64 位值,请参阅:Getting the high part of 64 bit integer multiplication(尽管它不支持矢量化形式)
【讨论】:
以上是关于将随机整数转换为范围 [min,max] 而不进行分支的主要内容,如果未能解决你的问题,请参考以下文章