_mm256_dp_ps
只适用于2到4个元素的点积运算;对于更长的向量,请在循环中使用垂直SIMD并在最后缩减为标量。在循环中使用_mm256_dp_ps
和_mm256_add_ps
会慢得多。
GCC和clang要求您启用(通过命令行选项)您使用内部函数的ISA扩展,而不像MSVC和ICC那样。
以下代码可能接近您的CPU的理论性能极限。未经测试。
使用clang或
gcc -O3 -march=native
进行编译。(至少需要
-mavx -mfma
,但
-mtune
选项由
-march
隐含,也可以使用其他
-mpopcnt
和其他
arch=native
启用的选项。调整选项对于大多数带有FMA的CPU来说是编译效率的关键,特别是
-mno-avx256-split-unaligned-load
:
为什么gcc不能将_mm256_loadu_pd解析为单个vmovupd?)
或者使用MSVC
-O2 -arch:AVX2
进行编译。
#include <immintrin.h>
#include <vector>
#include <assert.h>
template<int offsetRegs>
inline __m256 mul8( const float* p1, const float* p2 )
{
constexpr int lanes = offsetRegs * 8;
const __m256 a = _mm256_loadu_ps( p1 + lanes );
const __m256 b = _mm256_loadu_ps( p2 + lanes );
return _mm256_mul_ps( a, b );
}
template<int offsetRegs>
inline __m256 fma8( __m256 acc, const float* p1, const float* p2 )
{
constexpr int lanes = offsetRegs * 8;
const __m256 a = _mm256_loadu_ps( p1 + lanes );
const __m256 b = _mm256_loadu_ps( p2 + lanes );
return _mm256_fmadd_ps( a, b, acc );
}
float dotProductFma( const std::vector<float>& a, const std::vector<float>& b )
{
assert( a.size() == b.size() );
assert( 0 == ( a.size() % 32 ) );
if( a.empty() )
return 0.0f;
const float* p1 = a.data();
const float* const p1End = p1 + a.size();
const float* p2 = b.data();
__m256 dot0 = mul8<0>( p1, p2 );
__m256 dot1 = mul8<1>( p1, p2 );
__m256 dot2 = mul8<2>( p1, p2 );
__m256 dot3 = mul8<3>( p1, p2 );
p1 += 8 * 4;
p2 += 8 * 4;
while( p1 < p1End )
{
dot0 = fma8<0>( dot0, p1, p2 );
dot1 = fma8<1>( dot1, p1, p2 );
dot2 = fma8<2>( dot2, p1, p2 );
dot3 = fma8<3>( dot3, p1, p2 );
p1 += 8 * 4;
p2 += 8 * 4;
}
const __m256 dot01 = _mm256_add_ps( dot0, dot1 );
const __m256 dot23 = _mm256_add_ps( dot2, dot3 );
const __m256 dot0123 = _mm256_add_ps( dot01, dot23 );
const __m128 r4 = _mm_add_ps( _mm256_castps256_ps128( dot0123 ), _mm256_extractf128_ps( dot0123, 1 ) );
const __m128 r2 = _mm_add_ps( r4, _mm_movehl_ps( r4, r4 ) );
const __m128 r1 = _mm_add_ss( r2, _mm_movehdup_ps( r2 ) );
return _mm_cvtss_f32( r1 );
}
可能的进一步改进:
1. 使用8个向量而不是4个进行展开。我已经检查了
gcc 9.2 asm output,编译器只使用了16个可用的向量寄存器中的8个。
2. 确保输入向量都对齐,例如使用自定义分配器,在msvc上调用_aligned_malloc / _aligned_free,或在gcc和clang上使用aligned_alloc / free。然后将_mm256_loadu_ps替换为_mm256_load_ps。
要自动向量化一个简单的标量点积,你还需要使用OpenMP SIMD或者`-ffast-math`(由`-Ofast`隐含)让编译器将浮点数运算视为可结合的,即使实际上并不是(因为舍入误差)。但是GCC在自动向量化时不会使用多个累加器,即使它进行了展开,所以你的瓶颈将是FMA延迟,而不是加载吞吐量。
(每个FMA需要2个加载操作,这意味着这段代码的吞吐量瓶颈是向量加载,而不是实际的FMA运算。)
2023年更新:由于这个答案收到了很多赞,这里提供另一个版本,支持任意长度的向量,不一定是32个元素的倍数。主循环是一样的,不同之处在于处理余数的方式。
正如你所见,以一种既高效又公平的方式处理余数是相对棘手的,这涉及到求和的顺序对结果的数值精度有影响。实现的关键部分是_mm256_maskload_ps
条件加载指令。
#include <immintrin.h>
#include <vector>
#include <algorithm>
#include <assert.h>
#include <stdint.h>
template<int offsetRegs>
inline __m256 fma8( __m256 acc, const float* p1, const float* p2 )
{
constexpr ptrdiff_t lanes = offsetRegs * 8;
const __m256 a = _mm256_loadu_ps( p1 + lanes );
const __m256 b = _mm256_loadu_ps( p2 + lanes );
return _mm256_fmadd_ps( a, b, acc );
}
#ifdef __AVX2__
inline __m256i makeRemainderMask( ptrdiff_t missingLanes )
{
missingLanes = std::max( missingLanes, (ptrdiff_t)0 );
uint64_t mask = ~(uint64_t)0;
mask >>= missingLanes * 8;
__m128i tmp = _mm_cvtsi64_si128( (int64_t)mask );
return _mm256_cvtepi8_epi32( tmp );
}
#else
static const int alignas( 64 ) s_remainderLoadMask[ 16 ] = {
-1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0 };
inline __m256i makeRemainderMask( ptrdiff_t missingLanes )
{
missingLanes = std::max( missingLanes, (ptrdiff_t)0 );
missingLanes = std::min( missingLanes, (ptrdiff_t)8 );
const int* rsi = &s_remainderLoadMask[ missingLanes ];
return _mm256_loadu_si256( ( const __m256i* )rsi );
}
#endif
template<int offsetRegs>
inline __m256 fma8rem( __m256 acc, const float* p1, const float* p2, ptrdiff_t rem )
{
constexpr ptrdiff_t lanes = offsetRegs * 8;
const __m256i mask = makeRemainderMask( ( 8 + lanes ) - rem );
const __m256 a = _mm256_maskload_ps( p1 + lanes, mask );
const __m256 b = _mm256_maskload_ps( p2 + lanes, mask );
return _mm256_fmadd_ps( a, b, acc );
}
float dotProductFma( const std::vector<float>& a, const std::vector<float>& b )
{
assert( a.size() == b.size() );
const size_t length = a.size();
if( length == 0 )
return 0.0f;
const float* p1 = a.data();
const float* p2 = b.data();
const ptrdiff_t rem = ( ( length - 1 ) % 32 ) + 1;
const float* const p1End = p1 + length - rem;
__m256 dot0 = _mm256_setzero_ps();
__m256 dot1 = _mm256_setzero_ps();
__m256 dot2 = _mm256_setzero_ps();
__m256 dot3 = _mm256_setzero_ps();
while( p1 < p1End )
{
dot0 = fma8<0>( dot0, p1, p2 );
dot1 = fma8<1>( dot1, p1, p2 );
dot2 = fma8<2>( dot2, p1, p2 );
dot3 = fma8<3>( dot3, p1, p2 );
p1 += 32;
p2 += 32;
}
dot0 = fma8rem<0>( dot0, p1, p2, rem );
dot1 = fma8rem<1>( dot1, p1, p2, rem );
dot2 = fma8rem<2>( dot2, p1, p2, rem );
dot3 = fma8rem<3>( dot3, p1, p2, rem );
dot0 = _mm256_add_ps( dot0, dot2 );
dot1 = _mm256_add_ps( dot1, dot3 );
dot0 = _mm256_add_ps( dot0, dot1 );
__m128 r4 = _mm_add_ps( _mm256_castps256_ps128( dot0 ),
_mm256_extractf128_ps( dot0, 1 ) );
r4 = _mm_add_ps( r4, _mm_movehl_ps( r4, r4 ) );
r4 = _mm_add_ss( r4, _mm_movehdup_ps( r4 ) );
return _mm_cvtss_f32( r4 );
}
dpps
指令。对于更大的数组,你需要使用垂直FMA运算到多个累加器中(参考:为什么Haswell架构上mulss只需要3个周期,与Agner's instruction tables不同?(使用多个累加器展开FP循环))。你需要使用-mfma -mavx2
或者更好的-march=native
选项。是的,你需要启用目标选项来使用任何内嵌函数,并且加上-O3
选项。 - Peter Cordes