n方过百万 暴力碾标算——指令集优化的基础使用

ouuan

2019-02-01 16:53:36

Personal

摘要(好像洛谷日报要求有这个..):通过指令集可以在OJ上使用复杂度错误的算法A掉一类题目。虽然对提升水平无益,然而可以偶尔用来艹标算娱乐一下。 欢迎到[我的hexo博客](https://ouuan.github.io/n%E6%96%B9%E8%BF%87%E7%99%BE%E4%B8%87-%E6%9A%B4%E5%8A%9B%E7%A2%BE%E6%A0%87%E7%AE%97%E2%80%94%E2%80%94%E6%8C%87%E4%BB%A4%E9%9B%86%E4%BC%98%E5%8C%96%E7%9A%84%E5%9F%BA%E7%A1%80%E4%BD%BF%E7%94%A8/)阅读! 感谢 yfz 和 mcfx 在 WC 营员交流上的分享! 然而只看那个课件来学习指令集好像略有困难..所以我来分享一下~~我自学一晚上的成果~~。 希望能帮助大家暴力过题,~~考场上再也写不出标算~~。 你问学了指令集之后如何保持对标算的信仰?~~当然是每天%神橡树~~ ok不扯了,进入正题... > 注:本文省略了无数个 `unsigned`,请自行把所有 `int` 视作 `unsigned int`,把所有 `long long` 视作 `unsigned long long`。 # 适用范围 ## 环境 **不要尝试在OI竞赛中使用指令集优化。** 只适用于提供资瓷的OJ,具体列表参照营员交流ppt: ![](https://i.loli.net/2019/02/01/5c54095d8aac1.png) ![](https://i.loli.net/2019/02/01/5c54095dce2af.png) sse2,avx 什么的都是指令集的名字。 ## 作用 适用于**方便对连续内存空间进行批量处理**的题目。大约可以视作每 $8$ 个 int 为一个分块,块内进行赋值、修改等操作常数为 $1$,也就实现了常数/=$8$。当然如果是 long long 就只能除以四。 # 具体使用 ## pragma&include ```cpp #pragma GCC optimize("Ofast,no-stack-protector,unroll-loops,fast-math") #pragma GCC target("sse,sse2,sse3,ssse3,sse4.1,sse4.2,avx,avx2,popcnt,tune=native") #include <immintrin.h> #include <emmintrin.h> ``` 第一行是优化,如果你都用指令集了当然是能优化尽量优化。 第二行是告诉编译器你要使用指令集。 后面两个头文件是 C++ 将指令集封装成了函数,这样就不用在代码中内联汇编了。 ## 变量类型 大约有 `__m256i` `__m256` `__m256d` 三种,分别存储 `long long`,`float` 和 `double`,实际上 `__m256i` 也可以用来存储 `int`。 ~~实际使用的时候由于`__m256i`打起来比较麻烦,建议 `typedef __m256i oak`~~ ## 指令使用 可以在[一个神奇的网站](https://software.intel.com/sites/landingpage/IntrinsicsGuide)查需要的指令,左边选指令集以及指令类型,右边是指令,点开指令可以查看函数原型以及伪代码。 这里列几条常用指令: `__m256i _mm256_set_epi32 (int e7, int e6, int e5, int e4, int e3, int e2, int e1, int e0)`:参数是八个数,也就是一个“分块”里的数,注意是逆序的。返回值是一个含这八个数的“分块”。 `__m256i _mm256_set_epi64x (__int64 e3, __int64 e2, __int64 e1, __int64 e0)`:和上面一样,只不过是 $64$ 位整数,也就是 long long。 `__m256i _mm256_set1_epi32 (int a)`:相当于 `_mm256_set_epi32(a,a,a,a,a,a,a,a)`。 `__m256i _mm256_add_epi32 (__m256i a, __m256i b)`:把两个“分块”的对应位置分别相加,返回结果。 `__m256i _mm256_cmpeq_epi32 (__m256i a, __m256i b)`:判断两个“分块”的对应位置是否相等,若相等则返回的“分块”对应位置是 `0xffffffff`,否则是 `0`。 `__m256i _mm256_cmpgt_epi32 (__m256i a, __m256i b)`:和上面一样,只不过比较符是大于而不是相等。 `__m256i _mm256_and_si256 (__m256i a, __m256i b)`:返回两个“分块”的按位与,可以配合上面两条比较指令来使用。 ## 访问数据 可以直接通过下标访问: ```cpp #include <cstdio> #pragma GCC optimize("Ofast,no-stack-protector,unroll-loops,fast-math") #pragma GCC target("sse,sse2,sse3,ssse3,sse4.1,sse4.2,avx,avx2,popcnt,tune=native") #include <immintrin.h> #include <emmintrin.h> typedef __m256i oak; oak a; int main() { a=_mm256_set_epi32(1,2,3,4,5,6,7,8); printf("%d",a[2]); return 0; } ``` 你们可以猜猜这个的结果是什么。 答案是..4。 为什么呢,首先 `_mm256_set_epi32` 的参数是逆序的,所以实际上存储的数顺序是 `8,7,6,5,4,3,2,1`。其次,`__m256i` 类型是存储 long long 的,所以直接通过下标访问实际上是在访问 long long,如果 `cout<<a[2]`就会返回 `12884901892`($3\times2^{32}+4$)。所以,这句话实际上是在 `printf("%d",12884901892ll);`。 那么如何访问 `int`(甚至 `short`,如果题目允许这样就可以常数除以 $16$)呢? 其实搞个指针就可以了: ```cpp a=_mm256_set_epi32(1,2,3,4,5,6,7,8); int *b=(int *)&a; printf("%d",b[2]); ``` 这样子的输出就是 $6$ 了。 用这种方法就可以方便地处理序列问题了: ```cpp #include <cstdio> #pragma GCC optimize("Ofast,no-stack-protector,unroll-loops,fast-math") #pragma GCC target("sse,sse2,sse3,ssse3,sse4.1,sse4.2,avx,avx2,popcnt,tune=native") #include <immintrin.h> #include <emmintrin.h> typedef __m256i oak; int A[80],*b; oak a[10],x; int main() { int i; for (i=0;i<80;++i) scanf("%d",A+i); for (i=0;i<10;++i) a[i]=_mm256_set_epi32(A[i*8+7],A[i*8+6],A[i*8+5],A[i*8+4],A[i*8+3],A[i*8+2],A[i*8+1],A[i*8]); b=(int *)&a; x=_mm256_set1_epi32(233); for (i=0;i<10;++i) a[i]=_mm256_add_epi32(a[i],x); for (i=0;i<80;++i) printf("%d ",b[i]); return 0; } ``` 上面是一个简单的示例,读入 $80$ 个数,然后输出它们加上 $233$ 的结果。 # 例题 ~~这种东西为什么还会有例题啊。~~ [教主的魔法](https://www.luogu.org/problemnew/show/P2801),这题比较简单(~~废话暴力当然简单~~)。 [【模板】线段树1](https://www.luogu.org/problemnew/show/P3372),这题其实是最简单的,然而由于 dl 出题人把值域搞到了 long long,常数只能除以四,需要卡卡常,多提交几次才能过。 [[Ynoi2018]五彩斑斓的世界](https://www.luogu.org/problemnew/show/P4117),神司怒艹lxl标算的课件例题。 [Simple Tree](http://uoj.ac/problem/435),这个还要树剖,只不过也还好,神司是直接内嵌汇编写的,没有测过用函数能不能过.. 然后以教主的魔法为例讲一下代码: ```cpp #include <cstdio> #pragma GCC optimize("Ofast,no-stack-protector,unroll-loops,fast-math") #pragma GCC target("sse,sse2,sse3,ssse3,sse4.1,sse4.2,avx,avx2,popcnt,tune=native") #include <immintrin.h> #include <emmintrin.h> const int N=1000010; typedef __m256i oak; int n,m,tot,*a; oak A[N>>3]; char op[10]; void modify(int l,int r,int x) { while ((l&7)&&l<r) a[l++]+=x; //处理左边不是整块的部分,和分块的处理方法是一样的 if (l==r) return; while (r&7) a[--r]+=x; //处理右边不是整块的部分 if (l==r) return; oak t=_mm256_set1_epi32(x); //剩下的部分整块加上x for (l>>=3,r>>=3;l<r;++l) A[l]=_mm256_add_epi32(A[l],t); } int query(int l,int r,int x) { int out=0; while ((l&7)&&l<r) out+=int(a[l++]>=x); //处理左边不是整块的部分 if (l==r) return out; while (r&7) out+=int(a[--r]>=x); //处理右边不是整块的部分 if (l==r) return out; oak t=_mm256_set1_epi32(1); //这个1是每个大于等于x的数的贡献 oak ans=_mm256_set1_epi32(0); //这个ans是用来存答案的 oak cp=_mm256_set1_epi32(x-1); //这个是用来比较的,题目中是大于等于,所以和x-1比较 for (l>>=3,r>>=3;l<r;++l) ans=_mm256_add_epi32(ans,_mm256_and_si256(t,_mm256_cmpgt_epi32(A[l],cp))); //这个意会一下,作用是数当前块有几个大于x-1的数 for (int i=0;i<4;++i) out+=(ans[i]&0xffffffff)+(ans[i]>>32); //最后统计答案,因为ans[i]是一个long long,所以要前32位和后32位分别统计 return out; } int main() { int i,l,r,x,aa[8]; scanf("%d%d",&n,&m); while (n) //读入比较鬼畜,需要每次读8个数 { if (n<8) { for (i=0;i<n;++i) scanf("%d",aa+i); n=0; } else { for (i=0;i<8;++i) scanf("%d",aa+i); n-=8; } A[tot++]=_mm256_set_epi32(aa[7],aa[6],aa[5],aa[4],aa[3],aa[2],aa[1],aa[0]); } a=(int*)&A; while (m--) { scanf("%s%d%d%d",op,&l,&r,&x); if (op[0]=='M') modify(l-1,r,x); else printf("%d\n",query(l-1,r,x)); } return 0; } ```