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

How to read optimally from an array (in memory) having array position from a vector?

我有这样的代码:

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 内部,基本上是定义为 __m128 的 4 对齐浮点数。 这部分代码,经过一些基准测试后,需要一些时间(我猜是因为缺少很多缓存)。

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

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

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

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

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

  3. 最后,处理器可以用一条指令加载 64 位值。您正在从 table 加载连续对,可以使用 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, _mm_extract_epi32( posInt, 2 ), _mm_extract_epi32( posInt, 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, 0, 2, 0 ) );
    const __m128 v1 = _mm_shuffle_ps( s01, s23, _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. Modern compilers even automatically use FMA, when available。 (GCC 默认情况下,clang 与 -ffp-contract=fast 进行跨 C 语句的收缩,而不仅仅是在一个表达式中。)


刚看到更新。考虑将目标切换到 SSE 4.1。 Steam 硬件调查显示 market penetration is 98.76%. If you still supporting prehistoric CPUs like Pentium 4, the workaround for _mm_floor_ps is in DirectXMath,您可以使用 _mm_srli_si128 + _mm_cvtsi128_si32.

而不是 _mm_extract_epi32

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