First of all you definitely don't want , unless you want to type-pun FP to int
1.
但无论如何,对于运行时变量索引,您可能不想分支以选择具有正确 imm8 的指令。
Godbolt 编译器资源管理器上 gcc/icc/clang/msvc 的源 + asm 输出 https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAKxAEZSAbAQwDtRkBSAJgCFufSAZ1QBXYskwgA5AHo5AagCqgvG3kBZAMoA1AMLyZAcQBu8iMczIi4pgwYBKecPkWrJQfKbFM8gA5NBQUx0eVV5b2BBDgAGAEEFZzQsAFpgTBZQjxUAWzxmYnkieQB3BCYCeQBPUTApELSKpgAzAkwC1QZVVWAAOhjY/u4AZlVkBhEseQ4h3Txs3JYCYlUehGnsQa4RljGJn2ndQQJ8RdX1zZGmliwm%2BQB9O%2BRfccFnkQ95T4UNHV1%2Bfky8lygW6hQQeEEpE81xKPnQqBYtQqLEwwVCFSaJHkf14ANYlWKTEqF1G40mByO6FseGALDOQ2wXxk8kxBV0tFonk6tICEHsUOcBHBHiSPlszmKJAA1h4Ati%2BnFhul8E1Boqtngmjd7ndYtoABoPfpNBiocryBq0LgADjuTGMAA8IA9slbrZ4oaoKnh0A77BwAOx8OKfT4ut14Fw%2Bh1TIYAEXu80exgIKiGXDuKjdEGj/qGPFDhdDCmM2VQxnQ/UL4Ztn0ECBETRNaOmCbuSd8bWyIlaxi8d18gggTChxlz0wLBmZxk7xFyDEHVdD3gIYgy7eyydTgjuTXTEHrjeb6DzwYGAbjF2VmrV8WZD3KSzwACMe5gHlACF4GhBuFxAkEQx/vYIFTvI8KYIIiJSBU%2BCCEwz4MD4er6qEty2N4TDoJU8jpAhSHoD08gAOqsK0IRFEEPiaJojKiqk6QKrEJpmhUlo2pmgSYEMzrtm6HqhIsoS%2Bo4gZnoWaAsEc8giF66Z3BUprFAALLGCbRA60RDNEXDRLQ0TRAKrS%2BMprYadEykWVZpn5kunyyYs8kVEESFuAUrbyEpqkAtGABURyYCZE52Tqro2pGY6%2BmpiabsgKZphmWY2gemCudYp4hQotitMQLDlHgFggJ5ZohEw8jKckz6VK0JSqPCxSCUU0Q9D0tAAKwssQqDZECmBlsQlRESU7g%2BMwrQ7MSIbFsyRz5cg8iSQFDq%2BAUDkEE5TgNk2SFRG1PCmW1CYeWJxUqVCXn8AFJm%2BfQp2mbwV3Kb5XDnagKmXcZT1DFMF7BVNTI1taEXRtFG5bglmZ4Nmh7bZBHB7dG8Nxhlir/aFEabUeBGg0mMPNncgV4NazpJsgAQEIOkPZkw/JRiJf2xIWK5rjF4M7nuXAk7F5NJbag4Hltx4gQzgaXqjAwalqmC3A8KEZncxqmuaDRcG1ABsdqOpzLqq2rAlesJfo/eJoaA8DUUeWDcWpng8m8zm9P5kWRYlmWFYhTr6vmw6us49kuuPDzUMcSouvmOOtni4WCioLOrAhCQWAFLHvioCoNVNF1PUBD1rJgj4qAMCEXrLFJeDzXl2SYENEC6MwagQVBSKeMgEi%2BBUQo%2BCiDWVz4xSEAgogVChbVWvIIAyP6aOe3rmOwyElvzAHs7dr2XjWg68n8yOdM%2B%2Brp5Mi4K%2BLmjcjM7lMUB9bgS7vueMEQfYFaHogLAioahChCIXn%2BupPxez%2B4NxX3JoOZeghoaCwfo/BQz99AQiBBCd%2BwAwRf0VL9U%2BzJY5tHjvIRObQgTlGQAgSCnhBC5yxJ3eQ3cqFMCrkVOqmAYxNDwMQI4UIRAsE6FKHwg4trPmhCEGcXY8gn0ZtNGedZIEtnjJfdWA4uxvgdH2YgA4hyRT3mrKENMJyHyEXOURSprg3nForViFpMAEDdHcbwSt0C8TCu6HeBtczG0ysyfuQpTr4OpLSKuQkcFHBIJgEQUI0DZF8HkNoWQCB5AYAtQeacfBFB8RkKq8h0zyGfJYJg7wknEJwlRfOThrCYBCik3ktA1aOBYuaAg4T4Y8GtEjBm1ZcYlNURAOpvgtEozEZ8H%2BhR6kI19M0yO54xYDHVJcbUssDRGjiAoNa7cVEVGyESTwxhUA%2BnCMELZagVC0lwQUAAXm0VAuEHTjRUAiFkWJIw4LwPwIYApURFNaEcJiNS2IWIDjYkq9jfY7yWUsBShtRJBjcbVTxSlvHchYH4xoMJAneBCQtbqESkKsOKbE%2BJqBEmFHOSkzJOEMlZLJrk/OBTXmUORaUtG5ShzpmqUrDuQzGmjJNmGJecjaUdK6T0lpy4LEsy6Q0xGh0RboIlkMa8qoTFTM1DMu4hgAByihdDzLvGi8JkSsUROQFKTwxSgkyD%2BVhYpxByiYGAJNZiLLzGWI4rSEQyB7H8ScUJFxYlv7CovkwMVIyJVO0hWMVgyDVy5Q8IQRq5zEZcDVkBDBFpW7aoxSQ6NjCmBWAYDhLhPgGjWNSiVQYUqSRSxlrqOZCs4hfIdQHZ1rqJEeu9CJVxaMBn%2BuGQ6DlUw0ETMMSqMCszDTVulbKodyq1UatHbeGQvl50LvnfIAAKtgTQy6SIAEll0AAlsQAHkVXrtiCqjdINYgADFV0ACV5CbpVQAGXvfeww8hF3vvfTIfoChN3gR9NBDukEKiLS/EJVw1hISZJ7PnbwnhYOoGfGOUQggc1%2BDTioRCBd25zDwMc/KCIPCd1gxAPNgksAOlBGVEDZE4OWsqFPSZWrQ1sFqFkKRuDsO5FOe0DwFgBoWjxaVGEEapK4T49VcEagvSxvmMEPAVq57Ng8KUdIaGQSYe/ZgojqbdWQZYKgZy5zuxEKYqY2pQHOKATdbWbREKk3MfDWuQjwoY1H2EQuYu8xrQ133QUMq%2BnkixyhQgQ2ZkGPR2ZMAFNuTZR%2BBhvwtZghDWZ26r1fqlQoSbt0PoHBsCgTlhkG7dAMheGNmfD6kTDqrEAW4sOKEXBemi1vMMRV0sdSqvVZq2t7yCB3AbdZxx/o7N9LAp/DwscYlcZIZRKRHg87Uc9SwXwPZlMDy5HEsJabWEVZZuxW0/WRwNamLwKrTqWAurq/IaIjWpXdYsw2x4QQBv609UNzlYEovzRwQ5418GdiYFeuagg/cJBDSy/oWlhGQfVx2xfPbfXzuuqcaJE78ODtQlCPwWgN3%2B1bFlc1mt9qeuFtsdrPiNm3uw4yPDs1djDso4LDTotWFLvXclf2hV5adQoS60Tiz9oHS0Ge7Z97ChqgiFqBRSThqhTmk27p4aIgi4ySCG5ucIiPAOnmFCLX2RPReaGjwaD%2BmoQGeIQUWXGQfudxwrYOJFLKHgZIMkSSSxC4uCiXgBEVPTu2gF3Vo7AJ4f%2B53mzsZTWTGSyVXLXnZjicC7J/7dWnhKftt9dTn5ciE91eUgzutWetaXaqezszgGjiax9ncLgrgAXJ53hI58npXveowQMlWBefY57z%2B3jW2fG%2BhGgZgzjtgioOdYx7y5lqrBNH4nnEcpkMGPPjDZScnwfc94r5zEceBu%2BZ974X/vO%2BdGjaAzsilqBbg0toT4e%2BPgnfEEhX2dA6BBwCPzhkBEPhGFLCzasmkCAKgskBMMAtFFJMByZgt5BkgmxyZkg1lPEAQ4xYgAAtfgK9ZApwc5FDN6eqUTFLHqQQEQZ8fTOccUBaVgZuMoXwTsFgEtXHGVIxOVRjO7cvWnDMZAJ7JtdhL0ZZUFI/YbCLZNeaXQWiZzG/EpSDM3KJcCBEJEJiQQjxELYFFZUIKET7N/a3EVYhY7Hgb6aTaETwZ/bwEENgOAjAAuJsIIAgNxSMbgNWDyAMQVfpdPfPDWWnOrHfHQ1wknf5JxJ5HHZrPHRgqQfkBgaQNqKQUgFgaQaISI1AaQHEPgE7YQMQCQY7IYWgSIggGIkI/kKUEAIYbzZSAMUeNqG0AATj0mUloHKPVkYGkGUkiOyDoAMiiJyNIHiKkEiMEBAEMmyKkFiP5DgFgBgEQBQHRV1XIEoHl0xRAGAGtHoGYQYByh6IgGfHaOfFUC8EqGkEyNIDCQRX3U4R2IGMiKwDWTYCQnaPwG8CsAKkgnaMYUsDfF2MiK9FSnaKfGaKkEyNCLDRQH%2BF4EYBfB6MgH5Amy9ykmkGSH3W%2BmSFIjsCgLWXEDWHjDKEEGKFSgYG6NEHEEkGx3qKkAiLaNOI6OkAdGtDVmSDVlUnUO805AgFwEICxGGFul0AmMxXSOx2xEBJ4CyJyJAlIHyO0h6GUgWIDGUi4FoADGtHKOUksnKMJMaNIGaLakMmiNJM6O6N6NIH6MGNIGGLGJmLaCmIgGNOIBAHLmQFoGtFICWJWMoHWNJM2LygGleP2O6kOOOOuL6jDSuNJJuMsBiQsB6NJKeOQBeJ%2BLeMWA%2BNJK%2BNeL%2BLYABNxCBM6GfFBL5A6OwwI2hNhKgIRLiTgK8CIVbHRMxLsBxNSPxNCPCMiI1NiLJKkApKpJpNCBTRtJ6GamiDMCZOsC5KhHZJ1U5OGCO0SP4H5NOMFOISwjaEoBrKkGVNVPVPaK1KEB1L1NyKFIKOajFOlMlOlNlPlOiEVLCKkCGDrJXOkAnP1MNNvKQHNNNPNJQHrmADVlaPtKiUdI2K2LdKjI9Nk0WCOJzR9IuOAH9IbMDLuJDMeIdGeNaHdPeNPIbPjKjMTPYF5OBPTPgHBOzKhKkGSAdHmnhLtygLzKLJRNLICHLOxKEFxIkDoHnOJPrLiPJMpOpNUh%2BzVk7M7J7PwD7NZIHI5PwWGCGEcDHN4GvM3OFNoE7OUgyIsnKO0nKNoCGFqLaiVKaJADVJJIbNXJ6L6IFINNGIgHvKErIAoDNLMufJqLuBpLtLyAdLWO/NdJOL2IOPSAICApOIgt9MuMkADJYSDPuNDIbPDMjL2MQs%2BOWG%2BN%2BMYH%2BM4BTIEDTIzJwpiRzPwv3RjCMFMGSGLKQDlkrLxIYsJKYsvKbLYtbLyxqPkF0mlPkGvWXWIl4uZPci2DZLMvSNzx5MSskqnPAKTjnI0pVK0uXM1KvLXIMsnLyIKJkosnkuUkUuPJUrUsJPPJ0pYq6N1IFPnK4AvLGs2o3MFL42uWiOUiAA对于这个答案中的所有功能。包括(在底部)一些使用编译时常量 idx 的测试调用程序,以便您可以看到在实际程序中发生内联 + 常量传播时会发生什么。和/或来自同一向量的两个索引(仅 gcc CSE 并从同一存储重新加载两次,其他编译器存储两次)。
使用 gcc/clang/ICC 进行存储/重新加载优化(但variable-idx版本的延迟较高)。其他方法只能很好地优化 clang 的持续输入。 (clang甚至可以看穿pshufb
版本并将其变成vshufps imm8
or vpermilps imm8
,或者 idx=0 时无操作)。其他编译器会做一些愚蠢的事情,例如将向量归零vxorps
并将其用作vpermilps
控制!
128 位向量:如果有 SSSE3,请使用变量洗牌pshufb
, or AVX
使用 AVX1,您只需 2 ALU uops 即可完成 128 位向量的操作vpermilps
,这是一个使用双字选择器元素的变量洗牌,与pshufb
.
这可以让您进行与您的操作完全相同的随机播放_mm_shuffle_ps
(包括将低位元素复制到高位 3 个元素,这很好),但使用运行时索引而不是立即数。
// you can pass vectors by value. Not that it matters when inlining
static inline
float get128_avx(__m128i a, int idx){
__m128i vidx = _mm_cvtsi32_si128(idx); // vmovd
__m128 shuffled = _mm_permutevar_ps(a, vidx); // vpermilps
return _mm_cvtss_f32(shuffled);
}
gcc 和 clang 对于 x86-64 (Godbolt 编译器资源管理器)如下编译:
vmovd xmm1, edi
vpermilps xmm0, xmm0, xmm1
ret
没有 AVX 但使用 SSSE3,您可以加载或创建掩码pshufb
。对 4 的数组进行索引是相当常见的__m128i
向量,特别是使用_mm_movemask_ps
结果作为索引。但这里我们只关心低 32 位元素,所以我们可以做得更好。
事实上,该模式的常规性质意味着我们可以使用两个 32 位立即数操作数通过乘法和加法来创建它。
static inline
float get128_ssse3(__m128 a, int idx) {
const uint32_t low4 = 0x03020100, step4=0x04040404;
uint32_t selector = low4 + idx*step4;
__m128i vidx = _mm_cvtsi32_si128(selector);
// alternative: load a 4-byte window into 0..15 from memory. worse latency
// static constexpr uint32_t shuffles[4] = { low4, low4+step4*1, low4+step4*2, low4+step4*3 };
//__m128i vidx = _mm_cvtsi32_si128(shuffles[idx]);
__m128i shuffled = _mm_shuffle_epi8(_mm_castps_si128(a), vidx);
return _mm_cvtss_f32(_mm_castsi128_ps(shuffled));
}
海湾合作委员会输出-O3 -march=nehalem
(其他编译器也这样做,模块可能浪费了movaps
):
get128_ssse3(float __vector(4), int):
imul edi, edi, 67372036 # 0x04040404
add edi, 50462976 # 0x03020100
movd xmm1, edi
pshufb xmm0, xmm1
ret # with the float we want at the bottom of XMM0
因此,如果没有 AVX,存储/重新加载会保存指令(和微指令),特别是如果编译器可以避免符号扩展或零扩展索引。
在 Core2(Penryn) 及更新版本的 Intel CPU 上,从 idx 到结果的延迟 = imul(3) + add(1) + movd(2) + pshufb(1)。从输入向量到结果的延迟仅为pshufb
, 尽管。 (加上 Nehalem 上的旁路延迟延迟。)http://agner.org/optimize/ http://agner.org/optimize/
__m256
256 位向量:使用 AVX2 进行随机播放,否则可能会存储/重新加载
与 AVX1 不同,AVX2 具有跨车道可变洗牌,例如vpermps http://felixcloutier.com/x86/VPERMPS.html。 (AVX1 仅具有整个 128 位通道的立即洗牌。)我们可以使用vpermps
作为 AVX1 的直接替代品vpermilps
从 256 位向量中抓取一个元素。
有两个内在函数vpermps
(See ).
-
_mm256_permutevar8x32_ps(__m256 a, __m256i idx)
:旧名称,操作数与 asm 指令的顺序相反。
-
_mm256_permutexvar_ps(__m256i idx, __m256 a)
:AVX512 引入的新名称,操作数顺序正确(与 asm 操作数顺序匹配,与_mm_shuffle_epi8
or _mm_permutevar_ps
). The asm指令集参考手册入门 http://felixcloutier.com/x86/VPERMPS.html仅列出此版本,并以错误的类型列出它(__m256 i
为控制操作数)。
gcc 和 ICC 仅在启用 AVX2(而非 AVX512)的情况下接受此助记符。但不幸的是 clang 只接受这个-mavx512vl
(or -march=skylake-avx512
),所以你不能便携地使用它。因此,只需使用更笨重的 8x32 名称,它在任何地方都适用。
#ifdef __AVX2__
float get256_avx2(__m256 a, int idx) {
__m128i vidx = _mm_cvtsi32_si128(idx); // vmovd
__m256i vidx256 = _mm256_castsi128_si256(vidx); // no instructions
__m256 shuffled = _mm256_permutevar8x32_ps(a, vidx256); // vpermps
return _mm256_cvtss_f32(shuffled);
}
// operand order matches asm for the new name: index first, unlike pshufb and vpermilps
//__m256 shuffled = _mm256_permutexvar_ps(vidx256, a); // vpermps
#endif
_mm256_castsi128_si256
从技术上讲,不会使上通道未定义(因此编译器永远不需要花费指令进行零扩展),但无论如何我们并不关心上通道。
这编译为
vmovd xmm1, edi
vpermps ymm0, ymm1, ymm0
# vzeroupper # these go away when inlining
# ret
因此,它在 Intel CPU 上非常棒,从输入向量到结果的延迟仅为 3c,吞吐量成本为 2 uops(但两个 uops 都需要端口 5)。
AMD 上的车道交叉洗牌成本要高得多。
存储/重新加载
存储/重新加载实际上很好的情况:
-
没有 AVX2 的 256 位向量,或没有 SSSE3 的 128 位向量。
-
如果您需要同一向量中的 2 个或更多元素(但请注意,如果您实际调用,除 gcc 之外的编译器会多次存储
get128_reload
。因此,如果您这样做,请手动内联向量存储并对其进行多次索引。)
当ALU端口压力(尤其是shuffle端口)成为问题时,吞吐量比延迟更重要。在英特尔 CPU 上,movd xmm, eax
也在端口 5 上运行,因此它与 shuffle 竞争。但希望您只在内部循环之外使用标量提取,周围有许多代码可以执行其他操作。
When idx
通常是一个编译时常量,您希望让编译器为您选择洗牌。
A bad idx
不过,它可能会使你的程序崩溃,而不仅仅是给你错误的元素。将索引直接转变为随机播放控制的方法会忽略高位。
当心ICC 有时会在内联后错过将常量索引优化为随机播放的情况 https://stackoverflow.com/questions/51407959/get-an-arbitrary-float-from-a-simd-register-at-runtime/51413278?noredirect=1#comment89799186_51413278。 ICC 没问题test_reload2
在 Godbolt 的例子中。
存储/重新加载到本地阵列对于吞吐量来说完全没问题(也许不是延迟),并且由于存储转发,在典型 CPU 上只有约 6 个周期的延迟。大多数 CPU 比矢量 ALU 具有更多的前端吞吐量,因此,如果您接近 ALU 吞吐量而不是存储/加载吞吐量的瓶颈,那么在混合中包含一些存储/重新加载一点也不坏。
宽的存储可以转发到窄的重新加载,但受到一些对齐限制。我认为向量的 4 个或 8 个元素中的任何一个的自然对齐双字重新加载在主流 Intel CPU 上都很好,但您可以查看 Intel 的优化手册。请参阅性能链接x86 标签维基 https://stackoverflow.com/tags/x86/info.
在 GNU C 中,您可以像数组一样索引向量。如果内联后索引不是编译时常量,它会编译为存储/重新加载。
#ifdef __GNUC__ // everything except MSVC
float get128_gnuc(__m128 a, int idx) {
return a[idx];
// clang turns it into idx&3
// gcc compiles it exactly like get_reload
}
#endif
# gcc8.1 -O3 -march=haswell
movsx rdi, edi # sign-extend int to pointer width
vmovaps XMMWORD PTR [rsp-24], xmm0 # store into the red-zone
vmovss xmm0, DWORD PTR [rsp-24+rdi*4] # reload
完全可移植的编写方式(256 位版本)是:
float get256_reload(__m256 a, int idx) {
// with lower alignment and storeu, compilers still choose to align by 32 because they see the store
alignas(32) float tmp[8];
_mm256_store_ps(tmp, a);
return tmp[idx];
}
编译器需要多条指令来对齐函数的独立版本中的堆栈,但是当然在内联之后,这种情况只会发生在外部包含函数中,希望在任何小循环之外。
您可以考虑分别存储向量的高/低半部分vextractf128
和 128 位vmovups
,就像 GCC 所做的那样_mm256_storeu_ps
当它不知道目的地是否对齐时,对于tune=generic(帮助Sandybridge和AMD)。这将避免对 32 字节对齐数组的需要,并且对 AMD CPU 基本上没有任何缺点。但与对齐存储相比,英特尔的情况更糟,因为假设对齐堆栈的成本可以分摊到许多 get() 操作上,它会花费额外的 uops。 (函数使用__m256
有时无论如何最终都会对齐堆栈,所以你可能已经付出了成本。)你可能应该只使用对齐的数组,除非你只针对 Bulldozer、Ryzen 和 Sandybridge 等进行调整。
脚注1:_mm_extract_ps
将 FP 位模式返回为int
。底层 asm 指令 (extractps r/m32, xmm, imm8 http://felixcloutier.com/x86/EXTRACTPS.html) 对于将浮点数存储到内存可能很有用,但不能将元素洗牌到 XMM 寄存器的底部。这是 FP 版本pextrd r/m32, xmm, imm8
.
所以你的函数实际上是将整数位模式转换为 FP,并使用编译器生成的cvtsi2ss
,因为 C 允许隐式转换int
to float
.