如何从具有向量数组位置的数组在内存中中最佳读取?

问题描述

我有这样的代码

const rack::simd::float_4 pos = phase * waveTable.mLength;
const rack::simd::int32_4 pos0 = pos;
const rack::simd::float_4 frac = pos - (rack::simd::float_4)pos0;
rack::simd::float_4 v0;
rack::simd::float_4 v1;
for (int v = 0; v < 4; v++) {
    v0[v] = waveTable.mSamples[pos0[v]];
    v1[v] = waveTable.mSamples[pos0[v] + 1]; // mSamples size is waveTable.mLength + 1 for interpolation wraparound
}

oversampleBuffer[i] = v0 + (v1 - v0) * frac;

在两个样本之间采用 phase(归一化)和插值(线性插值),存储在 waveTable.mSamples(每个作为单个浮点数)。

位置在rack::simd::float_4里面,基本上是4个对齐的浮点数,定义为__m128。 经过一些基准测试后,这部分代码需要一些时间(我猜是因为缺少大量缓存)。

使用 -march=nocona 构建,因此我可以使用 MMX、SSE、SSE2 和 SSE3。

你会如何优化这段代码?谢谢

解决方法

由于多种原因,您的代码效率不高。

  1. 您正在使用标量代码设置 SIMD 向量的各个通道。处理器不能完全做到这一点,但编译器假装他们可以。不幸的是,这些编译器实现的变通方法速度很慢,通常它们是通过往返内存来实现的。

  2. 通常,您应该避免编写长度非常短的 2 或 4 循环。有时编译器展开并且您没问题,但其他时候它们不会并且 CPU 错误预测了太多分支。>

  3. 最后,处理器可以用一条指令加载 64 位值。您正在从表中加载连续的对,可以使用 64 位加载而不是两个 32 位加载。

这是一个固定版本(未经测试)。这假设您是为 PC 构建的,即使用 SSE SIMD。

// Load a vector with rsi[ i0 ],rsi[ i0 + 1 ],rsi[ i1 ],rsi[ i1 + 1 ]
inline __m128 loadFloats( const float* rsi,int i0,int i1 )
{
    // Casting load indices to unsigned,otherwise compiler will emit sign extension instructions
    __m128d res = _mm_load_sd( (const double*)( rsi + (uint32_t)i0 ) );
    res = _mm_loadh_pd( res,(const double*)( rsi + (uint32_t)i1 ) );
    return _mm_castpd_ps( res );
}

__m128 interpolate4( const float* waveTableData,uint32_t waveTableLength,__m128 phase )
{
    // Convert wave table length into floats.
    // Consider doing that outside of the inner loop,and passing the __m128.
    const __m128 length = _mm_set1_ps( (float)waveTableLength );

    // Compute integer indices,and the fraction
    const __m128 pos = _mm_mul_ps( phase,length );
    const __m128 posFloor = _mm_floor_ps( pos );    // BTW this one needs SSE 4.1,workarounds are expensive
    const __m128 frac = _mm_sub_ps( pos,posFloor );
    const __m128i posInt = _mm_cvtps_epi32( posFloor );

    // Abuse 64-bit load instructions to load pairs of values from the table.
    // If you have AVX2,can use _mm256_i32gather_pd instead,will load all 8 floats with 1 (slow) instruction.
    const __m128 s01 = loadFloats( waveTableData,_mm_cvtsi128_si32( posInt ),_mm_extract_epi32( posInt,1 ) );
    const __m128 s23 = loadFloats( waveTableData,2 ),3 ) );

    // Shuffle into the correct order,i.e. gather even/odd elements from the vectors
    const __m128 v0 = _mm_shuffle_ps( s01,s23,_MM_SHUFFLE( 2,2,0 ) );
    const __m128 v1 = _mm_shuffle_ps( s01,_MM_SHUFFLE( 3,1,3,1 ) );

    // Finally,linear interpolation between these vectors.
    const __m128 diff = _mm_sub_ps( v1,v0 );
    return _mm_add_ps( v0,_mm_mul_ps( frac,diff ) );
}

程序集looks good。现代编译器甚至会自动使用 FMA,when available。 (默认情况下,GCC 使用 -ffp-contract=fast 叮当以跨 C 语句进行收缩,而不仅仅是在一个表达式中。)


刚看到更新。考虑将目标切换到 SSE 4.1。 Steam 硬件调查显示 market penetration is 98.76%。如果您仍然支持像 Pentium 4 这样的史前 CPU,_mm_floor_ps is in DirectXMath 的解决方法,而不是 _mm_extract_epi32,您可以使用 _mm_srli_si128 + _mm_cvtsi128_si32

即使您只需要支持像 SSE3 这样的旧基线,-mtune=generic 甚至 -mtune=haswell-march=nocona 一起可能是一个好主意,仍然可以进行内联和其他代码生成适用于各种 CPU 的选择,而不仅仅是 Pentium 4。