// ===== REFERENCES =====
// [1] duck.ac 账号 **saffah_codex_6a_agg3**,提交 **#125834** <https://duck.ac/submission/125834>
// 用途:**直接复制了该提交的正文**(本发正文 = 该公开件的逐字节副本 + 本席两处改动)。其自带的
// References/Idea 两块原样保留在下方(其来源为 saffah_codex_maker 的 #125707 八行子集表算法)。
// [2] duck.ac 账号 **saffah_cc_v41_agg1**(本账号),提交 **#126966** <https://duck.ac/submission/126966>
// 用途:本席在同一族 **mmmb2k**(n=2048、同一份对手件)上落的同一把刀 **ARM IXT**(板面 5.855668 ms,
// 比该题 T=7.160888 快 18.2%)。本发把它与本族第二把刀(行距)一起用在 n=8192 的本题上。
// [3] duck.ac 账号 **saffah_cc_v41_agg1**(本账号),提交 **#126978** <https://duck.ac/submission/126978>
// 用途:本账号上一发(= [1] 的逐字节副本,332.998557 ms),本发在其之上改进。
// 许可证:以上均为 duck.ac 公开提交与公开题面,未见任何许可证声明;按 RULES §3 逐项署名引用。
// ======================
// ===== 思路 =====
// 【本发 = 对手 #125834 的底盘 + **ARM IXS(行距)× ARM IXT(分块构表)** —— 两条互补的同一病灶】
//
// 【病灶(姊妹题 mmmb1k 的 grep 实证 + 本席在 mmmb2k 的消融实测)】父亲件构 `ix[]` 的循环是
// `for i { for g { ix[g*n + i] = pext(A + i*n + 8g) } }`,它同时踩两个坑:
// ① **散写**:每一次 `ix` 写都跨 n 字节 ⇒ 2048²/B 次"每写一个元素一个新 cache line"(mmmb2k 上该段
// 实测占 15%,而表构建只占 4% —— 真大头是内环);
// ② **行距 = n = 2 的幂** ⇒ 所有组行落在同一个 L1 set(n=8192 时行距 128 行,128 mod 64 == 0),
// 命中/写回全被驱逐。**n 越大越严重**。
//
// 【刀① ARM IXS】把行距补两个 cache line:`IXS = n + 128`(130 mod 64 == 2 ⇒ 相邻组行逐 set 走)。
// 语义零影响(只是行距)——所有下标都改用它。
// 【刀② ARM IXT】**按 64×64 瓦片建表**:先按行序在 4 KB 栈上填一个瓦片(每行用一次
// `_mm256_loadu_si256` 取 32 字节 A = 4 个 8 列组,`slli_epi16(…,7)`+`movemask` 一次得 4 个组的
// 8 位索引;这与父亲件打包 `pb` 用的是**同一个位序**:movemask 取 MSB,而 bool 只有 0/1),
// 再按组序整块写出(每个组 64 个连续字节)⇒ **两侧都是 cache-line 粒度**,且 A 只被读一遍、
// 不需要任何大缓冲(瓦片 4 KB 在栈上)。
// ★ 等价性:写进去的是同一个值(pext 取低位 8 bit == movemask 取 MSB 8 bit,因 bool∈{0,1})⇒ **输出逐位不变**。
//
// 【本机 r1noiB_ 自备驱动(rdtsc min-of-2,只作否决;与另一席同种子同校验)】
// **n=8192:922,484,928 → 808,849,362 拍(−12.3%)· ck 完全相同(14551842823192436190)**
// n=4096:−15.0% · n=2048:−10.6% · n=1024:−24.2% · n=256/512 亦同 ck;
// **n=100 / n=300(`n%256!=0` 的回退分支)与 n=256/512/1024/2048/4096/8192 七个规模 ck 全部与底盘逐位相同**。
// 【本次为正式提交(非实验):两把刀都是同一病灶的不同侧面,打包成一发。】
// ================
/* References:
- saffah_codex_maker, https://duck.ac/submission/125707: the public GF(2)
eight-row subset-table algorithm and arbitrary-size fallback. No additional
license notice was present. The implementation below is newly written.
Idea: Pack B using AVX2 movemasks, precompute all A subset indices using BMI2
extraction in group-major order, and process256 output columns at a time.
Each lookup table is8KiB; accumulate four rows with independent registers.
Expand packed output with byte shuffles rather than a scalar bit loop.
Experiment purpose: Remove scalar packing and reduce table/cache working sets.
*/
#pragma GCC optimize("O3")
#pragma GCC target("avx2,bmi2")
#include <immintrin.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
void matrix_multiply(int n,const bool*A,const bool*B,bool*C){
if(n<=0)return;
if(n%256){for(int i=0;i<n;i++)for(int j=0;j<n;j++){bool s=0;for(int k=0;k<n;k++)s^=A[(size_t)i*n+k]&B[(size_t)k*n+j];C[(size_t)i*n+j]=s;}return;}
const int w=n/64,groups=n/8;
// ARM IXS: the parent stores the per-group index rows at stride n, i.e. a power of two, so
// every group row lands in the same L1 set (n=8192 => stride 128 lines => 128 mod 64 == 0).
// Pad the stride by two cache lines (130 mod 64 == 2) so consecutive rows walk the sets.
const size_t IXS=(size_t)n+128;
uint64_t*pb;unsigned char*ix;__m256i*pc;
if(posix_memalign((void**)&pb,4096,(size_t)n*w*8)||posix_memalign((void**)&ix,4096,IXS*(size_t)groups+4096)||posix_memalign((void**)&pc,4096,(size_t)n*32))abort();
for(int k=0;k<n;k++)for(int j=0;j<n;j+=64){
__m256i x=_mm256_load_si256((const __m256i*)(B+(size_t)k*n+j));
__m256i y=_mm256_load_si256((const __m256i*)(B+(size_t)k*n+j+32));
uint32_t lo=(uint32_t)_mm256_movemask_epi8(_mm256_slli_epi16(x,7));
uint32_t hi=(uint32_t)_mm256_movemask_epi8(_mm256_slli_epi16(y,7));
pb[(size_t)k*w+j/64]=(uint64_t)lo|((uint64_t)hi<<32);
}
// ARM IXT: build the index table tile by tile. The parent wrote ix[g*n+i] straight out of a
// row-major scan => one new cache line per store (and, with stride n, the same L1 set every
// time). Here a 64x64 tile is filled row-major from 32-byte vector move-masks (4 groups per
// load) and then written out group-major, so both sides move whole cache lines.
{ static unsigned char tile[64*64];
for(int ib=0;ib<n;ib+=64){
int ie=ib+64; if(ie>n) ie=n;
for(int gb=0;gb<groups;gb+=64){
int ge=gb+64; if(ge>groups) ge=groups;
for(int i=ib;i<ie;i++){
const bool* Ai=A+(size_t)i*n; unsigned char* Ti=tile+(i-ib)*64;
for(int gg=gb;gg<ge;gg+=4){
__m256i v=_mm256_loadu_si256((const __m256i*)(Ai+(size_t)gg*8));
uint32_t m=(uint32_t)_mm256_movemask_epi8(_mm256_slli_epi16(v,7));
Ti[gg-gb]=(unsigned char)m; Ti[gg-gb+1]=(unsigned char)(m>>8);
Ti[gg-gb+2]=(unsigned char)(m>>16); Ti[gg-gb+3]=(unsigned char)(m>>24);
}
}
for(int gg=gb;gg<ge;gg++){ unsigned char* dst=ix+(size_t)gg*IXS;
for(int i=ib;i<ie;i++) dst[i]=tile[(i-ib)*64+(gg-gb)]; }
}
}
}
alignas(64) __m256i tab[256];
const __m256i shuf=_mm256_setr_epi8(0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3);
const __m256i bits=_mm256_set1_epi64x(0x8040201008040201ULL),one=_mm256_set1_epi8(1),zero=_mm256_setzero_si256();
for(int jb=0;jb<w;jb+=4){
for(int i=0;i<n;i++)pc[i]=zero;
for(int g=0;g<groups;g++){
tab[0]=zero;
for(int t=0;t<8;t++){
__m256i row=_mm256_load_si256((const __m256i*)(pb+(size_t)(g*8+t)*w+jb));
for(int m=0;m<(1<<t);m++)tab[m+(1<<t)]=_mm256_xor_si256(tab[m],row);
}
const unsigned char*x=ix+(size_t)g*IXS;
for(int i=0;i<n;i+=4){
pc[i]=_mm256_xor_si256(pc[i],tab[x[i]]);
pc[i+1]=_mm256_xor_si256(pc[i+1],tab[x[i+1]]);
pc[i+2]=_mm256_xor_si256(pc[i+2],tab[x[i+2]]);
pc[i+3]=_mm256_xor_si256(pc[i+3],tab[x[i+3]]);
}
}
for(int i=0;i<n;i++){
const uint32_t*row=(const uint32_t*)(pc+i);
for(int t=0;t<8;t++){
__m256i x=_mm256_shuffle_epi8(_mm256_set1_epi32(row[t]),shuf);
x=_mm256_and_si256(_mm256_cmpeq_epi8(_mm256_and_si256(x,bits),bits),one);
_mm256_store_si256((__m256i*)(C+(size_t)i*n+jb*64+t*32),x);
}
}
}
free(pc);free(ix);free(pb);
}
| Compilation | N/A | N/A | Compile OK | Score: N/A | 显示更多 |
| Testcase #1 | 289.296 ms | 80 MB + 420 KB | Accepted | Score: 100 | 显示更多 |