1. 程式人生 > >一些卡常技巧

一些卡常技巧

取模運算 n) urn 展開 ret 臨時 scan 速度 方法

什麽?你說這些東西沒用?

  那你就大錯特錯了。WC考過的東西怎麽可能沒用

開O2之後FFT會比不開快幾倍

  不開O2:NTT比FFT快
  開O2:FFT比NTT快

常數盡量聲明成常量

  有一道NTT的題,模數聲明成變量跑了\(1166\)ms,模數聲明成常量跑了不到\(300\)ms

//6s
const int p=10;
int main()
{
    open("orzzjt");
    int a;
    scanf("%d",&a);
    int i;
    for(i=1;i<=1000000000;i++)
        a=(a*a+10)%p;
    printf("
%d\n",a); return 0; }
//10s
int p=10;
int main()
{
    open("orzzjt");
    int a;
    scanf("%d",&a);
    int i;
    for(i=1;i<=1000000000;i++)
        a=(a*a+10)%p;
    printf("%d\n",a);
    return 0;
}

能用位運算盡量用位運算

  當然,編譯器大多數情況下會幫你優化掉。

少用除法和取模

  加法運算只要\(1\)個時鐘周期,乘法運算只要\(3\)個時鐘周期,而除法和取模運算要幾到幾十個時鐘周期。

  \(3\times 3\)

的矩陣乘法:邊加邊取模:\(27\)次取模運算;全部算完再取模:\(9\)次取模運算。

優化高位數組的尋址

  用指針保存上一次使用的地址,直接加偏移。

對於一個值的重復運算,存入臨時變量中

消除條件跳轉

  a:對於適合分治預測的數據,測得平均一次循環需要\(4.0\)個時鐘周期;對於隨機數據,測得平均一次循環需要\(12.8\)個時鐘周期。可見,分支預測錯誤的懲罰為\(2\times (12.8-4.0)=17.6\)個時鐘周期。

  b:用三元運算符重寫,讓編譯器生成一種基於條件傳送的匯編代碼。測得不論數據如何,平均一次循環只需要\(4.1\)個時鐘周期。

//a.cpp
void minmax1(int
*a,int *b,int n) { for(int i=1;i<=n;i++) if(a[i]>b[i]) { int t=a[i]; a[i]=b[i]; b[i]=t; } }
//b.cpp
void minmax2(int *a,int *b,int n)
{
    for(int i=1;i<=n;i++)
    {
        int mi=a[i]<b[i]?a[i]:b[i];
        int ma=a[i]<b[i]?b[i]:a[i];
        a[i]=mi;
        b[i]=ma;
    }
}

循環展開

  a:平均每個元素需要\(3.65\)個時鐘周期。

  b:平均每個元素需要\(1.36\)個時鐘周期。

  這樣能夠刺激CPU並行。

  當展開次數過多時,性能反而會下降,因為寄存器不夠用\(\longrightarrow\)寄存器溢出

  註意每部分要獨立以及處理非展開次數的倍數的部分

//a.cpp
double sum(double *a,int n)
{
    double s=0;
    for(int i=1;i<=n;i++)
    {
        s+=a[i];
    }
    return s;
}
//b.cpp
double sum(double *a,int n)
{
    double s0=0,s1=0,s2=0,s3=0;
    for(int i=1;i<=n;i+=4)
    {
        s0+=a[i];
        s1+=a[i+1];
        s2+=a[i+2];
        s3+=a[i+3];
    }
    return s0+s1+s2+s3;
}

編寫緩存友好的代碼

空間局部性好

  盡量使用步長為\(1\)的訪問模式,即訪問的內存是連續的。

  在遍歷高維數組是很重要

時間局部性好

  是內存訪問的工作集盡量小

  在統計整數二進制表示中\(1\)的個數時,分兩段查表有時不如分三段好。

避免使用步長為較大的\(2\)的冪的訪問模式

  避免緩存沖突。

  在狀壓DP、使用高位數組時很重要

  解決方法:把數組稍微開大一些

一些數據

類型 延遲(周期數)
CPU寄存器 \(0\)
TLB \(0\)
L1高速緩存 \(4\)
L2高速緩存 \(10\)
L3高速緩存 \(50\)
虛擬內存 \(200\)

  在某Intel Core i5 CPU上,有這些高速緩存:

高速緩存類型 訪問時間(周期) 高速緩存大小 相聯度 塊大小 組數
L1 I-Cache \(4\) \(32\)KB \(8\) \(64\)B \(64\)
L1 D-Cache \(4\) \(32\)KB \(8\) \(64\)B \(64\)
L2 Cache \(12\) \(256\)KB \(4\) \(64\)B \(512\)
L3 Cache \(50\) \(6\)MB \(12\) \(64\)B \(8192\)

  對於不同的\(n\)\(d\),反復調用這個程序,具有不同的時空局部性。

  容易得知,\(n\)越小,時間局部性越好,\(d\)越小,空間局部性越好。

int sum(int *a,int n,int d)
{
    int s=0;
    for(int i=0;i<n;i++)
        s+=a[i*d];
    return s;
}
空間局部性

  \(n\)足夠大時結果如下

  與理論相符

\(d\) \(1\) \(2\) \(3\) \(4\) \(8\) \(16\) \(32\) \(64\)
周期數 \(1.50\) \(2.34\) \(3.46\) \(4.73\) \(9.70\) \(15.00\) \(19.76\) \(20.26\)
時間局部性

  \(n=200\)時結果如下

\(d\) \(2^{19}\) \(2^{19}+1\)
周期數 \(159\) \(1.18\)

  這是為什麽呢?

  \(200\)個整數,顯然能在L1緩存裝得下?

  對於\(d=2^{19}\),每次內存訪問時,地址的後\(19\)位都是一樣的。

  根據CPU高速緩存的原理,這些地址必然會被映射到同一個組

  因此,緩存只有一組,\(159\)周期就是內存訪問速度。

  p.s.:後\(19\)位一樣的是虛擬地址,在映射成物理地址之後,由於操作系統的特性,也至少有後\(12\)位是一樣的。

一些卡常技巧