【学习笔记】builtin函数

280 篇文章 1 订阅

前言

看到 N O I \rm NOI NOI 系列支持 builtin \text{builtin} builtin 函数,喜出望外,赶紧去学了一下。拾来的别人的牙慧

免责声明:我是看别人说的可以用,一切后果由使用者自行承担。

注意 builtin \text{builtin} builtin 函数的参数都是 u n s i g n e d    i n t \tt unsigned\;int unsignedint 类型。函数名的末尾加上 l l \rm ll ll 则变为 u n s i g n e d    l o n g    l o n g \tt unsigned\;long\;long unsignedlonglong 类型。

最低二进制位

原意应该是 f i n d    f i r s t    s e t \rm find\; first\; set findfirstset,找到第一个被 s e t \rm set set 1 1 1 的二进制位。

__builtin_ffs,若参数为 n n n,返回值为 x x x,表示 2 x − 1 2^{x-1} 2x1 n n n 的最低二进制位。

为了某种统一性, n = 0 n=0 n=0 时的返回值为 x = 0 x=0 x=0

性能测试

测试环境为 学校机房的电脑 W i n d o w s 10 \rm Windows10 Windows10 系统 64 64 64 位机,配置 Inter(R) Core(TM) i5-9500T \text{Inter(R) Core(TM) i5-9500T} Inter(R) Core(TM) i5-9500T CPU @ 2.20GHz 2.21 GHz \text{CPU @ 2.20GHz 2.21 GHz} CPU @ 2.20GHz 2.21 GHz

参赛选手是我们最喜欢的查表法(预处理 2 16 2^{16} 216 以内的所有数的结果)和内置函数。

使用 m t 19937 \rm mt19937 mt19937 随机(两次的种子相同)生成 32 32 32 位无符号整形,进行 1 0 8 10^8 108 次查询,不开 O 2 O2 O2 时,查表法约 2.9 2.9 2.9 秒,内置函数约 2.4 2.4 2.4 秒;开启 O 2 O2 O2 后,即使进行 1 0 9 10^9 109 次查询,运行速度差异不到 0.1 0.1 0.1 秒。可见查表法极快,效率差异的原因或许是函数调用。

而使用 mt19937_64 \text{mt19937\_64} mt19937_64 生成 64 64 64 位无符号整形时,进行 5 × 1 0 8 5\times 10^8 5×108 次查询,开启 O 2 O2 O2 的情况下查表法仍然是以约 6 6 6 秒的成绩略落后于内置函数 5.4 5.4 5.4 秒的成绩。所以有了结论:随机数据下内置函数最快

当然,事实上二者的差距比较小。我的建议是用内置函数,因为方便。

最高二进制位

这个不能直接查。但是我们可以 c o u n t    l e a d i n g    z e r o \rm count\; leading\; zero countleadingzero,数前导零!

__builtin_clz,返回前导零的数量。注意它是从第 32 32 32 位(毕竟参数是无符号整形)开始数的。

2022/8/22   update \texttt{2022/8/22 update} 2022/8/22 update:参数为 0 0 0 会导致 runtime error \text{runtime error} runtime error,难以置信! sanitizer \text{sanitizer} sanitizer 也会提醒你这一点。

另:一位著名选手也有一种快速求最高二进制位的方法,而且是 O ( 1 ) \mathcal O(1) O(1) 的,无任何内联函数。虽然跑得更慢就是啦

二进制位计数

这是最常用的。它听上去很简单,却成了自由发挥想象力的舞台

下面是瞎讲一通。具体只要记住 p o p u l a t i o n    c o u n t \rm population\;count populationcount,即 __builtin_popcount 就好了。

查表法

一般自己的程序就这么实现——预处理一个 2 8 2^8 28 或者更长的表,按照每 8 8 8 位直接查表。

事实上还有一种离谱的写法:利用 宏递归展开,如

# define BIT2(n)       n,       n+1,       n+1,       n+2
# define BIT4(n) BIT2(n), BIT2(n+1), BIT2(n+1), BIT2(n+2)
# define BIT6(n) BIT4(n), BIT4(n+1), BIT4(n+1), BIT4(n+2)
# define BIT8(n) BIT6(n), BIT6(n+1), BIT6(n+1), BIT6(n+2)
static const uint8_t table[256] = {BIT8(0)};

每次考虑最高的 2 2 2 个二进制位,剩下的部分递归。

并行计算

从未听说过的技巧,但是极具想象力。考虑到这样一个事实: 2 k > k    ( k ∈ N ) 2^k>k\;(k\in\N) 2k>k(kN),也就是说,任意 k k k 位二进制数,二进制下 1 1 1 的数量可以直接存储到它原来所占用的二进制位里。

那么,类似 f f t \rm fft fft 去掉递归的方法,我们从最底层开始,逐步合并。最底层是只考虑 1 1 1 个二进制位,那么原本的 b i t \rm bit bit 就是二进制下 1 1 1 的数量。然后我们合并相邻的两个:

n = ( n & 0x55555555 ) + ( (n >> 1) & 0x55555555 );

也就是将两个相邻的块(此时块长为一)的值加在一起。得到的结果不会超出新的块长的二进制位,所以块长翻倍。此时我们继续

n = ( n & 0x33333333 ) + ( ( n >> 2 ) & 0x33333333 );
n = ( n & 0x0F0F0F0F ) + ( ( n >> 4 ) & 0x0F0F0F0F );
n = ( n & 0x00FF00FF ) + ( ( n >> 8 ) & 0x00FF00FF );
n = ( n & 0x0000FFFF ) + ( ( n >> 16 ) & 0x000FFFF );

就能得到最终结果了!真是富有创造力!

一级优化

观察到最后一步等价于 n = ( n   m o d   2 16 ) + n 2 16 n=(n\bmod 2^{16})+\frac{n}{2^{16}} n=(nmod216)+216n,考虑将其转化为 n   m o d   ( 2 16 − 1 ) n\bmod(2^{16}-1) nmod(2161) 。显然二者是相等的,因为结果不超过 32 32 32,模 2 16 − 1 = 65535 2^{16}-1=65535 2161=65535 也没什么问题。

二级优化

仍然运用上面的思考, n   m o d   ( 2 k − 1 ) n\bmod(2^k-1) nmod(2k1) 2 k − 1 > 32 2^k-1>32 2k1>32 时等价于 k k k 位为一组的二进制值相加。显然应当取 k = 6 k=6 k=6,如何实现呢?

第一步要解决长度为 3 3 3 的分组。事实上我们可以使用 n − ∑ i = 1 + ∞ ⌊ n 2 i ⌋ n-\sum_{i=1}^{+\infty}\lfloor\frac{n}{2^i}\rfloor ni=1+2in 得到任意数的二进制位 1 1 1 的个数,因为 2 k − 2 k − 1 − ⋯ − 2 0 = 1 2^k-2^{k-1}-\cdots-2^0=1 2k2k120=1,就恰好会贡献 1 1 1

那么第一步无非是

n = n - ( (n >> 1) & 033333333333 ) - ( (n >> 2) & 011111111111 );

由于是 3 3 3 位为一组,采用了更容易理解的八进制数字常量。然后接下来,并行计算与取模,放在一起即可。

return ( ( n + ( n >> 3 ) ) & 030707070707 ) % 63;

分支预测

__builtin_expect(exp,c) 表示表达式 e x p exp exp 的结果更可能是 c ∈ { 0 , 1 } c\in\{0,1\} c{0,1},一般常用于 i f \rm if if 语句的判断。例如

if(__builtin_expect(zxy == sister, true))
	puts("I've already know that it's definitely right!");

则编译器认为,这个 i f \rm if if 语句很可能会成立,那么在汇编中就成了:如果该语句不成立,跳过 i f \rm if if 内的语句块。由于该 i f \rm if if 很可能成立,所以跳转的次数就会很少。习得卡常新技巧!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值