3

我估计英特尔 CPU 的最大 FLOPs/s 的公式是

Max SP FLOPs/s = frequencey * 4 SSE(8AVX) * 2 (MAC) * number of cores (not HW threads)
Max DP FLOPs/s = 0.5 * Max SP FLOPs/s

MAC是指CPU可以同时进行一个SSE(AVX)乘法和加法。在我使用的系统上,负载下的最大频率是 2.66 GHz。它只有 SSE,核心数(不是硬件线程)为 4。这给出:Max SP FLOPs/s = 85.12 GFLOPs/s。

矩阵乘法的 FLOP 数是 approxiamelty 2*n*m*k。对于 n=1000 的方阵,即 2*10E9 FLOP(20 亿 FLOP)。一旦我知道了我可以估计 FLOPs/s 的时间。

但是,对于我自己的代码,我能得到的最好的结果是大约 40 SP GFLOPs/s,例如 n=1000。我得到与 Eigen 相同的结果。这大约是 45% 的效率。我对最大值的计算是否错误?对于大型密集矩阵乘法,英特尔 CPU 的最佳效率是多少?有没有人有一篇描述这个的论文?

我知道在 GPU 上效率可以超过 60%。 http://www.anandtech.com/show/6774/nvidias-geforce-gtx-titan-part-2-titans-performance-unveiled/3

编辑:对于 n=500,我得到了类似的结果,它很容易适合我系统的 12MB L3 缓存,因此缓存似乎不是限制因素(尽管也许我可以更有效地使用它)。

Edit2:Eigen Benchmarks 显示它与 MKL(对于 SSE)一样好。他们使用 Intel(R) Core(TM)2 Quad CPU Q9400 @ 2.66GHz。所以 2.66* 2(DP SSE) *2 MAC * 4 cores = 42.25 DP GFLOPs/s。你可以在情节上看到他们都得到了不到 20 个。像我这样的人大约有 45%。 http://eigen.tuxfamily.org/index.php?title=基准

http://ark.intel.com/products/35365/Intel-Core2-Quad-Processor-Q9400-6M-Cache-2_66-GHz-1333-MHz-FSB

Edit3:这是我给关心的人的代码。我可以得到比这更好的结果,但不会好多少。我将 Agner Fog 的矢量类用于 SEE/AVX。并将 Vec8f 设置为 float8 并将 Vec4d 设置为 double4

//SGEMM and AVX call MM_tile<float, float8>(nthreads, a, b, c, n, m, k);
template <typename ftype, typename floatn>
void GEMM_tile(const int nthreads, const ftype*A , const ftype* B, ftype* C, const int N, const int M, const int K) {       
    for(int i=0; i<N; i++) {
       for(int j=0; j<K; j++) {
           C[K*i + j] = 0;
       }
    }   
    const int nc = 32;
    const int kc = 32;
    const int mc = 32;
    omp_set_num_threads(nthreads);
    #pragma omp parallel for if(nthreads>1)
    for(int ii=0; ii<N; ii+=nc) {
        for(int jj=0; jj<K; jj+=kc)
            for(int ll=0; ll<M; ll+=mc) {
                const int nb = min(N-ii, nc);
                const int kb = min(K-jj, kc);
                const int mb = min(M-ll, mc);
                MM_block<ftype, floatn>(nb, mb, kb, &A[M*ii+ll], N, &B[K*ll+jj], K, &C[K*ii+jj], K );
            }
     }
}

template <typename ftype, typename floatn>
void MM_block(int n, int m, int k, const ftype *a, const int stridea, 
                                   const ftype *b, const int strideb,
                                   ftype *c, const int stridec ) {
    const int vec_size = sizeof(floatn)/sizeof(ftype);
    for(int i=0; i<n; i+=4) {
        for(int j=0; j<k; j+=vec_size) {
            Dot4x4_vec_block<ftype, floatn>(m, &a[strideb*i], &b[j], &c[stridec*i + j], stridea, strideb, stridec);
    }
}

template <typename ftype, typename floatn>
inline void Dot4x4_vec_block(const int n, const ftype *a, const ftype *b, ftype *c, const int stridea, const int strideb, const int stridec) {
    floatn tmp0, tmp1, tmp2, tmp3;
    load(tmp0, &c[stridec*0]);
    load(tmp1, &c[stridec*1]);
    load(tmp2, &c[stridec*2]);
    load(tmp3, &c[stridec*3]);

    ftype *a0_ptr = (ftype*)&a[stridea*0];
    ftype *a1_ptr = (ftype*)&a[stridea*1];
    ftype *a2_ptr = (ftype*)&a[stridea*2];
    ftype *a3_ptr = (ftype*)&a[stridea*3];
    for(int i=0; i<n; i++) {
        floatn breg = floatn().load(&b[i*strideb + 0]);

        floatn areg0 = *a0_ptr++;
        floatn areg1 = *a1_ptr++;
        floatn areg2 = *a2_ptr++;
        floatn areg3 = *a3_ptr++;

        tmp0 += areg0 * breg;
        tmp1 += areg1 * breg;
        tmp2 += areg2 * breg;
        tmp3 += areg3 * breg;
}
    tmp0.store(&c[stridec*0]);
    tmp1.store(&c[stridec*1]);
    tmp2.store(&c[stridec*2]);
    tmp3.store(&c[stridec*3]);
}
4

1 回答 1

0

通常,处理吞吐量的限制因素是内存带宽,尤其是在您的工作集不适合 CPU 缓存的情况下(您的 1000×1000 矩阵float将占用约 4 MB,而您的 CPU 可能有 2 MB L3 缓存)。在这种情况下,您的算法结构可能会对它的执行方式产生很大影响,但您通常会在某个时候碰壁,因为您正在等待来自某个内存层次结构中的更高级别。

此外,您的理论数字假设您有足够的指令而没有数据依赖性,可以让所有执行单元在每个周期都被分配任务。这在实践中可能很难做到。我不确定一般矩阵乘法的最佳吞吐量是多少,但请查看上一个问题,了解如何最大限度地提高指令吞吐量。

于 2013-04-16T14:11:28.297 回答