Vector256.Create和Avx2.BroadcastScalarToVector函数有什么区别?

3
例如,我想创建一个Vector256变量,并将所有元素初始化为指定的带符号整数,假设我的系统支持Avx2。.NET文档显示,使用Avx2广播标量使用_mm256_broadcastd_epi32_mm_broadcastd_epi32指令。 Vector256.Create生成哪个指令?它与上述指令相同吗?
int value = -1;
Vector256<int> v1 = Avx2.BroadcastScalarToVector256(&value);
Vector256<int> v2 = Vector256.Create(-1);
Debug.Assert(v1.Equals(v2)); // True

_mm256_broadcastd_epi32是C内置函数,而不是汇编指令。由于C#不会编译成C再转换为汇编语言,因此这是糟糕的文档说明...但是,如果可能的话,它将使用从内存中加载的vpbroadcastd,否则如果该值在寄存器中,则使用vmovd xmm0, ecx / vpbroadcastd ymm0, xmm0(如果该值在寄存器中)。参考链接:https://www.felixcloutier.com/x86/vpbroadcast。(或者使用AVX512VL,希望是`vpbroadcastd ymm0, ecx`)。有一种方法可以查看从C# JIT编译的汇编代码,我记得甚至有一个在线编译器浏览器类型的网站,但我忘了它是什么,因为我自己不使用C#。 - Peter Cordes
但无论如何,对于一个常量操作数,我期望像C编译器一样使用 _mm256_set1_epi32( value ) 通常会从内存常量中加载值(理想情况下是广播加载),或者对于特殊情况,如零或-1,使用 vpxor xmm0,xmm0,xmm0 来将 YMM0 清零,或者使用 vpcmpeqd ymm0,ymm0,ymm0 将寄存器设置为全1,而不会对旧值产生错误依赖。 - Peter Cordes
Vector256.Create需要一个常量参数或者其他什么东西吗? - Peter Cordes
1
@PeterCordes sharplab 这样 - harold
@harold:好的,谢谢。所以看起来BroadcastScalarToVector256真的想成为广播加载,并且将首先存储一个变量。Vector256.Create(func_arg)将会执行vmovd / vpbroadcastd ymm0, xmm0。https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKGIAYACY8gOgBEBLbAcwDsJcGDmFwBuGvSasASgFdeQ/DBYBJBVA69cwsRMbMWchRyWr1m7SJYANABxJx1CQGYmpBgGEGAbxoN/TK7yuNgAZjAMAGowYBjQpACsSAA8mhgAfAwAsuQAFACUPn4BJcQA7FExcVCJSCwesNgYMLlw5PmOJQC+xf69gQzBYRHRsfFJqQqZWaS5aQwAbtgANrIwhb7UJSUA9DvlDACCCwikLABCUBDYACZgeBgAyvfL2FAAKhCj1bW5AGRLVbrTrbfwHb7jOoNGBNFqAtYdfo9ahdIA= - Peter Cordes
1个回答

2
TL/DR: 当源数据在内存中时,请使用 BroadcastScalarToVector256,在其他情况下请使用 Vector256<int>.CreateBroadcastScalarToVector256 的文档说明指出它编译成以下汇编代码:VPBROADCASTD ymm, m32。当源标量在内存中时,这是您想要的,但如果源数据是寄存器,则需要往返于内存并返回。即使内存位于堆栈上,即 L1D 缓存上,这个往返过程的延迟略微较慢。 Vector256.Create( int ) 的文档没有说明它编译成什么,只说它对应于 C++ 中的 _mm256_set1_epi32 内置函数。这意味着 JIT 编译器可以自由地执行最有效的操作。
如果您调用 Vector256<int>.Create( 0 ),它应该编译成 vpxor ymm0, ymm0, ymm0 指令,因为该指令是将向量清零的快速方法。
当您调用 Vector256<int>.Create( -1 ) 时,它应该编译成 vpcmpeqd ymm0, ymm0, ymm0 指令或类似指令,这是因为编译器已知该值,vpcmpeqd 没有数据依赖性,并且可以快速完成任务。
当您传递一个变量时,Create 应该编译成类似于 vmovd xmm0, eax; vpbroadcastd ymm0, xmm0 的代码,这是两个指令,但仍然比往返于内存和返回更快。

该来回路程在延迟方面相对较慢,仅供参考,例如在Skylake上比“vmovd xmm0,ecx / vpbroadcastd ymm0,xmm0”高2个周期,但由于32位和64位广播加载是由负载端口处理的,可以避免洗牌uop。偶尔值得考虑以平衡吞吐量。(测试循环:NASM使用vmovd eax,xmm0完成往返旅行:https://godbolt.org/z/b1hxx3)。(“vpbroadcastb/w ymm,mem”加载另外需要1个周期的延迟,总共比vmovd/vpbroadcastw差3个周期。) - Peter Cordes
使用[rsp+disp8]寻址模式时,存储/重新加载也会增加2个字节的代码大小,或者在mov存储上使用REX前缀更糟糕。或者使用[rsp][rbp+disp8]且没有REX时大小相同。 - Peter Cordes
@PeterCordes 好的,我改了措辞。关于我的TL/DR建议,我认为大多数代码往往更容易在load/store上出现瓶颈,而不是在shuffles上。另一件事是上下文切换,如果你真的很不幸,操作系统可能会在两个指令之间切换/恢复线程,在这种情况下,数据将全部移动到RAM。 - Soonts
虽然通常ALU策略是好的,但请记住,很多瓶颈在于load/store的代码是因为缓存未命中而不是执行单元吞吐量。此外,请注意,即使存储最终未命中缓存并尝试提交到L1d缓存时,存储转发也可以工作(具有恒定延迟)。 (因此,它不会阻塞存储转发,但可能会在存储缓冲区填充等待提交时最终阻塞执行;您不希望使用一些随机的静态/全局变量,这些变量很可能在缓存中很少使用。) - Peter Cordes
所以,似乎应该始终使用Create,只要它可以编译为广播加载,当操作数需要加载时。(不是将其mov到整数寄存器/vmovd/vpbroadcastd ymm,xmm)。不确定BroadcastScalarToVector256的用例是什么;让编译器决定何时应该在寄存器中使用是使用编译器的一部分。 (尽管公平地说,它们并不完美,有时拥有一个工具来阻止它们做出错误的选择是很好的。) - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接