您如何将“while”迭代器转换为 simd 指令?
Posted
技术标签:
【中文标题】您如何将“while”迭代器转换为 simd 指令?【英文标题】:How would you convert a "while" iterator into simd instructions? 【发布时间】:2021-08-16 08:57:27 【问题描述】:这是我实际拥有的代码(对于标量代码),我已复制 (x4) 将数据存储到 simd 中:
waveTable *waveTables[4];
for (int i = 0; i < 4; i++)
int waveTableIindex = 0;
while ((phaseIncrement[i] >= mWaveTables[waveTableIindex].mTopFreq) && (waveTableIindex < kNumWaveTableSlots))
waveTableIindex++;
waveTables[i] = &mWaveTables[waveTableIindex];
当然,它根本不是“更快”。你会如何用 simd 做同样的事情,节省 cpu?任何提示/起点? 我在 SSE2。
这是计算的上下文。 每个波表的 topFreq 从最大谐波量(x2,由于 Nyquist)开始计算,然后在每个波表上乘以 2(稍后除以每个表可用的谐波数):
double topFreq = 1.0 / (maxHarmonic * 2);
while (maxHarmonic)
// fill the table in with the needed harmonics
// ... makeWaveTable() code
// prepare for next table
topFreq *= 2;
maxHarmonic >>= 1;
然后,在处理每个样本时,由于 osc 的频率(即相位增量),我需要“捕捉”要使用的正确波表:
freq = clamp(freq, 20.0f, 22050.0f);
phaseIncrement = freq * vSampleTime;
例如(vSampleTime = 1/44100,maxHarmonic = 500),30hz 是波表 0,50hz 是波表 1,以此类推
【问题讨论】:
这是一个非常小的代码 sn-p,它遗漏了包括各种相关定义在内的基本细节。尽管如此,waveTables
和 phaseIncrement
似乎都有 4 个元素。这不太可能足以证明 SIMD 操作的合理性。此外,对于这些类型的条件操作,您应该查看 AVX-512,它比您的 SSE2 基线更新了 5 代。 (跳过 SSE3、SSE4、AVX、AVX2)
@MSalters 在一个 osc(音频)内,每个样本处理 4 个语音,16 个语音,4x osc。它会经常处理 sn-p :)
您必须显示更多上下文(即minimal reproducible example)。 SIMD 优化通常从选择合适的数据结构开始(例如,SoA 而不是 AoS)。
再进一步看,我还注意到waveTableIindex < kNumWaveTableSlots
检查发生在之后它被用作数组索引。这几乎可以肯定是错误的顺序。但它显示了将此类算法移植到 SIMD 的复杂性;你需要有全貌。一个简单的解决方法是将mWaveTables[last].mTopFreq
设置为phaseIncrement
的最大可能值(即使其成为哨兵)。
这主要取决于 while 循环中的内容,以及正在检查的条件类型,因此这不是一个描述性很强的标题。例如线性搜索对 SIMD 来说很容易,而在前一次迭代中对计算具有串行依赖性的东西通常要困难得多(除非您可以并行执行 4 个独立的串行 dep 链,可能使用不同的数据布局来完成这项工作。)
【参考方案1】:
假设您的值是 FP32,我会这样做。未经测试。
const __m128 phaseIncrements = _mm_loadu_ps( phaseIncrement );
__m128i indices = _mm_setzero_si128();
__m128i activeIndices = _mm_set1_epi32( -1 );
for( size_t idx = 0; idx < kNumWaveTableSlots; idx++ )
// Broadcast the mTopFreq value into FP32 vector. If you build this for AVX1, will become 1 very fast instruction.
const __m128 topFreq = _mm_set1_ps( mWaveTables[ idx ].mTopFreq );
// Compare for phaseIncrements >= topFreq
const __m128 cmp_f32 = _mm_cmpge_ps( phaseIncrements, topFreq );
// The following line compiles into no instruction, it's only to please the type checker
__m128i cmp = _mm_castps_si128( cmp_f32 );
// Bitwise AND with activeIndices
cmp = _mm_and_si128( cmp, activeIndices );
// The following line increments the indices vector by 1, only the lanes where cmp was TRUE
indices = _mm_sub_epi32( indices, cmp );
// Update the set of active lane indices
activeIndices = cmp;
// The vector may become completely zero, meaning all 4 lanes have encountered at least 1 value where topFreq < phaseIncrements
if( 0 == _mm_movemask_epi8( activeIndices ) )
break;
// Indices vector keeps 4 32-bit integers
// Each lane contains index of the first table entry less than the corresponding lane of phaseIncrements
// Or maybe kNumWaveTableSlots if not found
【讨论】:
1°问题:建表时可以广播topFreq。这会更快吗?即加载单个浮点数并广播到 _m128 比直接加载 _m128 慢,对吗? @markzzz 如果没有 AVX,_mm_set1_ps
编译成 _mm_load_ss
+ _mm_shuffle_ps
godbolt.org/z/TP4zvMWMr 这些洗牌在 1 个延迟周期内很快,而该函数中的其余代码不会t 使用任何随机播放。带有_mm_broadcast_ss
的AVX 更好,但不是很多。加载 16 字节向量时,您需要读取 4 倍的缓存行。并非总是如此,但我认为在这个特定的情况下,额外的 RAM 带宽比两条保存的指令更昂贵(两条是因为负载会合并到比较中)。
谢谢。试过你的代码,但我没有看到任何收益,而是像我发布的那样编写“标量”代码。我猜编译器已经做了一些很棒的事情......
@markzzz 假设分析器告诉您有问题的 for
循环是什么需要时间,您可以尝试的一种优化是重构数据结构,以便 mTopFreq
值在内存中是连续的。这样for
循环将从顺序地址加载,一旦找到所有 4 个索引,使用它们来索引另一个指针,其中包含这些表中的有效负载。
@markzzz RAM 现在是块设备。在 CPU 内部,块大小为 64 字节(高速缓存行),在芯片外部通常为 16 字节(DDR3 和 DDR4 模块每个事务提供 8 字节,而精心构建的计算机具有双通道 RAM)。如果您将修改为float topFreqs[kNumWaveTableSlots];
,您的代码将需要更少的内存块来完成循环。还改进了缓存,topFreqs
表有更高的机会适合并留在 L1D 缓存中。【参考方案2】:
在 C++ 中没有编写 SIMD 指令的标准方法。只要您将编译器配置为以支持此类指令并启用相关优化的 CPU 为目标,编译器就可以在适当的时候生成 SIMD 指令。您可以使用 std::execution::unsequenced_policy
使用标准算法来帮助编译器了解 SIMD 是合适的。
【讨论】:
Inline assembly 是另一种选择,尽管依赖编译器优化和分析通常会更好。 @Yun:这几乎可以肯定是过度的。 SSE 内在函数是一个更好的选择。让编译器处理寄存器分配等等。【参考方案3】:如果您使用的是 GCC/G++ 或 Clang,则向量扩展有一个非标准的语言扩展。使用__attribute__ ((vector_size (xx)))
。详见 GCC 手册
https://gcc.gnu.org/onlinedocs/gcc-11.2.0/gcc/Vector-Extensions.html#Vector-Extensions
【讨论】:
以上是关于您如何将“while”迭代器转换为 simd 指令?的主要内容,如果未能解决你的问题,请参考以下文章
使用 iPhone 的 SIMD 浮点单元将浮点数转换为整数
ARMv8 SIMD和浮点指令编程Libyuv I420 转 ARGB 流程分析