优化 x86 的逻辑与运算

Posted

技术标签:

【中文标题】优化 x86 的逻辑与运算【英文标题】:Optimizing a logic AND operation for x86 【发布时间】:2014-11-26 15:46:18 【问题描述】:

我正在尝试优化屏蔽数组的算法。初始代码如下所示:

void mask(unsigned int size_x, unsigned int size_y, uint32_t *source, uint32_t *m)

    unsigned int rep=size_x*size_y;
    while (rep--)
                
        *(source++) &= *(m++);
    

我尝试过循环展开+预取

void mask_LU4(unsigned int size_x, unsigned int size_y, uint32_t *source, uint32_t   *mask)
                             // in place
    unsigned int rep;
    rep= size_x* size_y;
    rep/= 4 ; 
    while (rep--) 
                  
        _mm_prefetch(&source[16], _MM_HINT_T0);
        _mm_prefetch(&mask[16], _MM_HINT_T0);
        source[0] &= mask[0];
        source[1] &= mask[1];
        source[2] &= mask[2];
        source[3] &= mask[3];
        source += 4;
        mask += 4;
    

并使用内在函数

void fmask_SIMD(unsigned int size_x, unsigned int size_y, uint32_t *source, uint32_t *mask)
                             // in place
    unsigned int rep;
    __m128i *s,*m ;
    s = (__m128i *) source;
    m = (__m128i *) mask;
    rep= size_x* size_y;
    rep/= 4 ; 
    while (rep--) 
                   
        *s = _mm_and_si128(*s,*m);
        source+=4;mask+=4; 
        s = (__m128i *) source;
        m = (__m128i *) mask;
       
  

但是性能是一样的。我尝试对 SIMD 和 Loop Unrolling 版本执行 sw 预取,但我看不到任何改进。关于如何优化此算法的任何想法?

P.S.1:我使用的是 gcc 4.8.1,并使用 -march=native-Ofast 进行编译。

P.S.2:我使用的是 Intel Core i5 3470 @3.2Ghz,Ivy 桥架构。 L1 DCache 4X32KB(8 路),L2 4x256,L3 6MB,RAM-DDR3 4Gb(双通道,DRAM @798,1Mhz)

【问题讨论】:

我怀疑数组足够大,以至于您实际上是在测量内存带宽。对此你无能为力,除了“不做”。 新 CPU 不会自动预加载吗? @Jake'Alquimista'LEE 现在连老家伙都可以了 @harold 是正确的。这是内存带宽限制。而且由于源和目标是相同的流指令将无济于事。但是你可以使用多个线程。与许多人认为单线程不会使主内存带宽饱和的情况相反。 在使用内部函数等之前,您是否检查了生成的代码?它很可能已经被矢量化了。 【参考方案1】:

您的操作受内存带宽限制。但是,这并不一定意味着您的操作正在达到最大内存带宽。要接近最大内存带宽,您需要使用多个线程。使用 OpenMP(将 -fopenmp 添加到 GCC 的选项中)您可以这样做:

#pragma omp parallel for
for(int i=0; i<rep; i++)  source[i] &= m[i]; 

如果您不想修改源而是使用不同的目标,那么您可以使用这样的流指令:

#pragma omp parallel for
for(int i=0; i<rep/4; i++) 
    __m128i m4 = _mm_load_si128((__m128i*)&m[4*i]);
    __m128i s4 = _mm_load_si128((__m128i*)&source[4*i]);
    s4 = _mm_and_si128(s4,m4);
    _mm_stream_si128((__m128i*i)&dest[4*i], s4);

这不会比使用相同的目标和源更快。但是,如果您已经计划使用不等于源的目标,这可能会比使用_mm_store_si128 更快(对于rep 的某些值)。

【讨论】:

感谢您的回答。实际上,我已经尝试应用流指令,使用不同的目的地而不使用多线程,并且没有区别。不幸的是,对于我的项目,多线程不是一种选择。但是我有一个问题:如何验证我的掩码算法是否受内存带宽限制?我可以为此使用分析器吗?如果是的话,你能推荐一个吗? @Nick,你的硬件是什么?您使用的 CPU 到底是什么? @Nick,您的算法受内存带宽限制,因为 CPU 比主内存快得多,而且您不重用数据。解决问题的最佳方法是不要像 Harold 所说的那样这样做。改变你的算法。不要一次处理整个数组。与其他计算一起分块进行。 @Nick,但如果您必须这样做,那么您应该找到硬件的理论最大带宽,然后将其与您测量的带宽进行比较。这可以告诉您通过大量努力可能可以获得多少。 @Nick,您的算法正在执行两次整数读取和一次整数写入,因此您测量的带宽为 3*sizeof(int)*rep/time【参考方案2】:

您的问题可能是内存受限,但这并不意味着您每个周期不能处理更多。通常当你有一个低负载操作时(就像你在这里,它毕竟只是一个 AND ),结合许多加载和存储是有意义的。在大多数 CPU 中,大多数负载将由 L2 缓存组合成单个缓存线负载(特别是如果它们是连续的)。我建议在这里增加循环展开到至少 4 个 SIMD 数据包,以及预取。您仍然会受到内存限制,但您会获得更少的缓存未命中,从而获得更好的性能。

【讨论】:

在源代码中展开(或在编译器生成的 asm 中展开,这是一个单独的问题)减少了循环开销,但与硬件预取是否能够跟上几乎没有关系。现代 x86 CPU 具有 OoO 执行来隐藏加载延迟,因此您不需要特别需要执行软件流水线以远远领先于使用结果的存储加载。没有足够的寄存器来产生有意义的差异,而编译器在展开时已经可以做到这一点。与存储交错的加载不会产生额外的缓存未命中。

以上是关于优化 x86 的逻辑与运算的主要内容,如果未能解决你的问题,请参考以下文章

深入了解CPU两大架构ARM与X86

使用逻辑运算符高效检索论文

逻辑运算与移位运算

关系运算符逻辑 运算符与三元运算符

2.3-X86指令简介

Java 逻辑运算符与短路逻辑运算符