AVX512 比特测试和运行性能推荐

AVX512 Performance Recommendation on Bit Test and Operation

我遇到了一组代码,其中 "kernel" 作为性能障碍。由于我可以使用最新的 Intel(R) Xeon Phi(TM) CPU 7210 (KNL),我希望使用 AVX512 intrinsic 来加速它。

for( int y = starty; y <= endy; y++)
{
    // hence data[][] is "unsigned char" while result[] is "int"
    for( int x = startx; x <= endx; x++)
    {
        if( (data[y][x]&0x1) == 0 )
            result[x] += data[y][x];
    }
}

在分析代码的行为后,我发现内部循环的长度大多小于 16,所以我写了以下内容

register int xlen = xend - xstart + 1;

__m512i zero5 = _mm512_setzero_si512();
__m256i zero2 = _mm512_castsi512_si256(zero5);
__m128i zero1 = _mm512_castsi512_si128(zero5);
__m256i mask2 = _mm256_set1_epi8(0x1);
__m128i mask1 = _mm256_castsi256_si128(mask2);

register __m512i psprof0 = zero5;

for( int i = 0; i < (16-xlen)&(~0x1); i += 2 ) mask1 = _mm_srli_si128(mask1, 2);
if( (16-xlen)&(0x1) ) mask1 = _mm_srli_si128(mask1, 1);

#pragma vector nontemporal
#pragma prefetch data
for( int y = starty; y <= endy; y++ )
{
    __m128i pixel16  = _mm_loadu_si128((__m128i*)&data[y][startx]);

    // if ( _mm_testc_si128(pixel16, mask1) ) continue;

    __m128i mask16    = _mm_andnot_si128(pixel16, mask1);
    __m128i pixel16n  = _mm_sign_epi8(pixel16, mask16);
            psprof0   = _mm512_add_epi32(psprof0, _mm512_cvtepu8_epi32(pixel16n));
}

_mm512_storeu_si512(&result[startx], psprof0);  

这里有几个问题:

  1. 由于_mm_srli_si128不接受非立即数参数,我不得不在那里使用循环,请问有什么办法可以消除它吗?
  2. _mm_testc_si128(pixel16, mask1) 大多对性能没有帮助,这当然是由于数据分布[][];但是,"Compute the bitwise NOT of a and then AND with b, and set CF to 1 if the result is zero, otherwise set CF to 0",有什么方法可以得到"ANDNOT"的结果,这样我就不用再计算_mm_andnot_si128了?
  3. 由于内环长度大多小于16,可能不太适合AVX512;但是,通过加载 data[y][x] 和 data[y+1][x],然后将它们组合成一个 __m256i 是否值得将 y 间隔展开 2 是否有意义?但是,由于 8 位整数到 16 位整数的转换在 KNL (AVX512BW) 上尚不可用,因此它可能比当前版本更令人沮丧。
  4. 总的来说,非常感谢任何 recommendations/suggestions 来提高 KNL 上这一小段代码的性能:)(它已经在 OpenMP 循环区域内,因此现在可能不可用)

以上第 3 点:

static inline __m256i vec_256_combine_128(__m128i a, __m128i b)
{
// combine two __m128i into one __m256i
return _mm256_insertf128_si256(_mm256_castsi128_si256(a), b, 1);
}

static inline __m128i vec_256_add_128(__m256i a)
{
// add lower 128bit and higher 128bit of __m256i consists of epi16
return _mm_add_epi16(_mm256_castsi256_si128(a), _mm256_extracti128_si256(a, 1));
} 

for( int y = starty; y <= endy; y += 2 )
{
    __m128i pixel16a  = _mm_load_si128((__m128i*)&pEdgeImage[y][sx]);
    __m128i pixel16b  = _mm_load_si128((__m128i*)&pEdgeImage[y+1][sx]);
    if ( y == ye ) 
        pixel16b  = zero1;  

    __m256i pixel16   = vec_256_combine_128(pixel16a, pixel16b);

    if ( _mm256_testc_si256(pixel16, mask1) ) continue;

    __m256i mask16    = _mm256_andnot_si256(pixel16, mask1);
    __m256i pixel16n  = _mm256_sign_epi8(pixel16, mask16);

    __m256i pixel16lo = _mm256_unpacklo_epi8(pixel16n, zero2);
    __m256i pixel16hi = _mm256_unpackhi_epi8(pixel16n, zero2);

            psprof0   = _mm256_add_epi16(psprof0, vec_256_combine_128(vec_256_add_128(pixel16lo), vec_256_add_128(pixel16hi)));
}

这里是一个线性版本,return 一个规范化的位计数(8 个浮点数),它被条目的数量规范化。 (手戳,很可能有一两个错字)

PURE FUNCTION BITS8(nGot, DataIn, nBits) BIND(C, NAME='BITS8')
USE OMP_LIB
USE, INTRINSIC :: IOS_C_BINDING, ONLY: C_FLOAT, C_INT8_Y, C_INT
IMPLICIT NONE
INTEGER(C_INT)                    , INTENT(IN) :: nGot
INTEGER(C_INT8_T)                 , INTENT(IN) :: nBits !Which should come in as 8
INTEGER(C_INT8_T), DIMENSION(nGot), INTENT(IN) :: DataIn
!DIR$ ATTRIBUTES ALIGN : 64                    :: Bits
REAL(C_FLOAT), DIMENSION(nBits)                :: Bits8

Bits8 = 0.0E0

!DIR$ ASSUME_ALIGNED DataIn:2
!DIR$ PREFETCH       DataIn:1:64
Sum_Loop: DO I = 1, nGot
!$OMP SIMD REDUCTION(+:Bits8) LINEAR(DataIn) SAFELEN(64)
  Bit_Loop: DO J = 0, nBits-1
    Bits8(J+1) = IBITS(DataIn(I),J, 1) + Bits8(J+1)
  ENDDO Bit_Loop
ENDDO Sum_Loop
!$OMP END

!DIR$ SIMD
Norm_Loop: DO J = 1, nBits
  Bits8(J) = Bits8(J)/nGot
ENDDO Norm_Loop

RETURN
END FUNCTION Bits8

你用'ifort -openmp -O3'等编译它。 显然,对于 2 数组,您需要#rows 和#columns,以及要检查的行和列的开始和结束位置。我相信你知道行和列在 c 和 fortran 中是相反的。

要在 .o 文件上使用 'nm' 计算结尾的下划线戏剧,BIND(C, NAME=) 也可以提供帮助。

也许你可以使用更精简的东西,然后在你的 C 中内联函数,并将 SIMD REDUCTION 放在 C 端。如果您在 'c side'.

上处理数组,您的优势是不必担心 row/column 差异
PURE FUNCTION BITS8(DataIn) BIND(C, NAME='BITS8')
USE OMP_LIB
USE, INTRINSIC :: IOS_C_BINDING, ONLY: C_INT8_T, C_INT
IMPLICIT NONE
INTEGER(C_INT8_T), PARAMETER      :: nBits = 8
INTEGER(C_INT8_T)    , INTENT(IN) :: DataIn
INTEGER(C_INT), DIMENSION(nBits)  :: Bits8

! Bits = 0.0E0

!DIR$ ASSUME_ALIGNED DataIn:2
!DIR$ PREFETCH       DataIn:1:64
Bit_Loop: DO J = 0, nBits-1
  Bits8(J+1) = IBITS(DataIn(I),J, 1)
ENDDO Bit_Loop
!$OMP END

RETURN
END FUNCTION Bits8

另一种方式是这样的:

PURE FUNCTION BITS8(nrows, ncols, startrow, startCol, EndRow, EndCol, DataIn) BIND(C, NAME='BITS8')
USE OMP_LIB
USE, INTRINSIC :: IOS_C_BINDING, ONLY: C_FLOAT, C_INT8_Y, C_INT
IMPLICIT NONE
INTEGER(C_INT8_T)                  , PARAMETER        :: nBits = 8
INTEGER(C_INT)                           , INTENT(IN) :: nRows
INTEGER(C_INT)                           , INTENT(IN) :: nCols
INTEGER(C_INT)                           , INTENT(IN) :: StartRow
INTEGER(C_INT)                           , INTENT(IN) :: StartCol
INTEGER(C_INT)                           , INTENT(IN) :: EndRow
INTEGER(C_INT)                           , INTENT(IN) :: EndCol
INTEGER(C_INT8_T), DIMENSION(ncols,nrows), INTENT(IN) :: DataIn
!DIR$ ATTRIBUTES ALIGN : 64                           :: Bits8
INTEGER(C_INT), DIMENSION(nBits)                      :: Bits8
INTEGER(C_INT)                                        :: I, J, K

!DIR$ ASSUME_ALIGNED DataIn:64
Bits8 = 0

Row_Loop: DO J = StartCol, EndCol
!DIR$ PREFETCH       DataIn:1:64
  Col_Loop: DO I = StartRow, EndRow
    !$OMP SIMD REDUCTION(+:Bits8) LINEAR(DataIn) SAFELEN(64)
    Bit_Loop: DO K = 0, nBits-1
      Bits8(K+1) = IBITS(DataIn(I,J),K, 1) + Bits8(K+1)
    ENDDO Bit_Loop
  ENDDO Sum_Loop
ENDDO Sum_Loop
!$OMP END

RETURN
END FUNCTION Bits8

除此之外,我认为您的数据[y][x]&0x1 应该能够始终使用某些#pragma vector 或#pragma simd(等)... -vec-report 3 应该允许您解决。

如果没有,那么小部分,内联可能是最好的??

我不知道你需要什么,但在第一个例子中,我在单核上获得了 >250 MB/second 的位吞吐量......所以你知道会发生什么。

我坚信最好的方法就是对数据进行直方图绘制。然后对每个直方图索引值进行位测试,并乘以该 bin 的直方图 bin 计数。当然,对于大计数值来说它更快。一旦您知道每个直方图索引的位模式,该部分就永远不会改变。所以对于大 'summing count numbers' 和小 'byte sizes' 肯定会更快。对于小计数大小和 64 位或更大,使用 IBITS 可能更快。

直方图在 9 月 16 日左右的英特尔网络研讨会上进行了介绍(c 和 fortran)。

fortran 的一个优点是对于单个字节,可以将直方图的尺寸标注为 (-128:128),这使得将值放入正确的 bin 中变得简单。