当前位置: 首页 > 知识库问答 >
问题:

动态生成向量常量的最佳指令序列是什么?

郏扬
2023-03-14

“最佳”是指最少的指令(或最少的uop,如果任何指令解码到多个uop)。以字节为单位的机器代码大小是相等insn计数的分界符。

常量生成本质上是新依赖链的开始,因此延迟很重要。在循环中生成常量也很不寻常,因此吞吐量和执行端口需求也大多无关紧要。

生成常量而不是加载常量需要更多的指令(除了全部零或全部一),因此它确实会消耗宝贵的uop缓存空间。这可能是比数据缓存更有限的资源。

Agner Fog的优秀优化装配指南在第13.4节中介绍了这一点。表13.10给出了生成向量的顺序,其中每个元素是0、1、2、3、4、1或2,元素大小从8位到64位。表13.11具有生成某些浮点值的序列(<代码>0.0,<代码>0.5,<代码>1.0,<代码>1.5,<代码>2.0,<代码>2.0,以及符号位的位掩码。)

Agner Fog的序列只使用SSE2,无论是出于设计还是因为它已经有一段时间没有更新了。

使用不明显的短指令序列可以生成哪些其他常量?(带有不同移位计数的进一步扩展很明显,并不“有趣”。)是否有更好的序列生成Agner Fog列出的常数?

如何将128位立即数移动到XMM寄存器说明了将任意128b常量放入指令流的一些方法,但这通常是不明智的(它不会节省任何空间,并且会占用大量uop缓存空间)

共有1个答案

何浩荡
2023-03-14

全部为零:px或xmm0,xmm0(或xorps xmm0,xmm0,短一个指令字节。)在现代CPU上没有太大区别,但在Nehalem上(在xor零消除之前),xorps uop只能在端口5上运行。我认为这就是为什么编译器支持pxor,即使对于将与FP指令一起使用的寄存器也是如此。

全能:pcmpeqw xmm0, xmm0。这是生成其他常量的通常起点,因为(如pxor)它打破了对寄存器先前值的依赖(K10和pre-Core2 P6等旧CPU除外)。

在Agner Fog的指令表中,与任何CPU上的字节或dword元素大小版本的pcmpeq相比,W版本没有任何优势,但pcmpeqQ需要额外的字节,在Silvermont上速度较慢,需要SSE4.1。

SO没有真正的表格格式,所以我只是列出Agner Fog表13.10的新增内容,而不是改进版本。抱歉。也许如果这个答案变得流行,我会使用ascii-art表格生成器,但希望改进将被纳入指南的未来版本。

Agner Fog的表生成16位元素的向量,并使用packuswb来解决这个问题。例如,pcmpeqw xmm0,xmm0生成一个向量,其中每个字节都是2。(这种具有不同计数的移位模式是为更宽的向量生成大多数常量的主要方式)。有一种更好的方法:

paddb xmm0,xmm0(SSE2)以字节粒度左移一位,因此只需两条指令(pcmpeqw/paddb)即可生成一个字节向量<代码>paddw/d/q对于其他元素大小,左移一次可以节省一个字节的机器代码,并且通常可以在比移位imm更多的端口上运行。

pbusb xmm0, xmm0(SSSE3)将全1向量(-1)转换为1字节的向量,并且是非破坏性的,因此您仍然拥有set1(-1)向量。

(您有时不需要set1(1)。您可以通过用psubb减去-1来为每个元素添加1。)

我们可以使用pcmpeqw/paddb/pabbs生成2字节。(add vs. abs的顺序并不重要)。pabs不需要im8,但仅在其他元素宽度与右移都需要3字节VEX前缀时为它们保存代码字节。这仅在源寄存器为xmm8-15时发生。(vpbusb/w/d始终需要VEX.128.66.0F38的3字节VEX前缀。WIG,但vpsrlw est, src, imm可以为其VEX. NDD.128.66.0F. WIG使用2字节VEX前缀。

我们实际上也可以保存生成4字节的指令:pcmpeqw/pbusb/psllw xmm0,2。由于pbusb,所有由word-shift跨字节边界移位的位都是零。显然,其他移位计数可以将单个设置位放在其他位置,包括生成-128(0x80)字节向量的符号位。请注意,pbusb是无损的(目标操作数是只写的,不需要与源相同即可获得所需的行为)。您可以将all-one保留为常量,或作为生成另一个常量的开始,或作为psubb的源操作数(以1递增)。

也可以使用packsswb从饱和到-128的任何内容生成0x80字节的向量(见上一段)。e、 g.如果你已经有了一个向量0xFF00,只需复制它并使用packsswb即可。从内存中加载的恰好饱和的常量是这方面的潜在目标。

可以使用pcmpeqw、psrlw xmm0、9生成0x7f字节的向量。我认为这是“不明显的”,因为大多数设置的性质并没有让我想到只是在每个单词中生成一个值,然后执行通常的packuswb。

pavgb(SSE2)对零寄存器可以右移1,但仅当值为偶数时。(它执行无符号的dst=(dst src 1)

我已经测试了这些序列。最简单的方法是将它们放入。asm、组装/链接并在其上运行gdb<代码>布局asm,<代码>显示/x$xmm0。v16\u int8在每一步和每一步指令(nisi)后转储。在布局reg模式下,您可以切换到矢量reg的显示,但这几乎是无用的,因为您无法选择要显示的解释(您总是获得所有解释,并且无法滚动,并且列在寄存器之间不对齐)。不过,对于整型regs/flags来说,它非常好。

请注意,将这些与intrinsics一起使用可能很棘手。编译器不喜欢对未初始化的变量进行操作,因此您应该使用_mm_undefined_si128()来告诉编译器这就是您的意思。或者使用_mm_set1_epi32(-1)可能会让您的编译器发出pcmpeqd相同,相同的。如果没有这个,一些编译器会在使用前xor-零未初始化的向量变量,甚至(MSVC)从堆栈中加载未初始化的内存。

通过利用SSE4.1的零扩展或符号扩展,许多常量可以更紧凑地存储在内存中。例如,可以通过从32位内存位置加载pmovzx来生成作为32位元素的128b向量。内存操作数可以与pmovzx微融合,因此它不需要任何额外的融合域UOP。但它确实阻止了将常量直接用作内存操作数。

C/C内部函数支持将pmovz/sx用作加载非常糟糕:有mm\u cvtep8\u epi32(\u m128i a),但没有采用uint32\u t*指针操作数的版本。您可以绕过它,但它很难看,编译器优化失败是一个问题。有关详细信息和gcc错误报告的链接,请参阅链接问题。

使用256b和(并非如此)512b常量,节省的内存更大。不过,只有当多个有用的常量可以共享一条缓存线时,这才非常重要。

与此等效的FP是VCVTPH2PS xmm1、xmm2/m64,需要F16C(半精度)特性标志。(还有一个存储指令,它将单个数据打包到一半,但没有半精度的计算。它只是一个内存带宽/缓存占用优化。)

显然,当所有元素都相同(但不适合动态生成)时,pshufd或AVX VBroadcasts都很有用<代码>pshufd可以接受内存源操作数,但必须是128b<代码>MOVDUP(SSE3)执行64位加载、广播以填充128b寄存器。在Intel上,它不需要ALU执行单元,只需要加载端口。(类似地,dword大小及更大的AVX广播加载在加载单元中处理,无ALU)。

当您要将掩码加载到寄存器中以在循环中重复使用时,广播或pmovz/sx非常适合节省可执行大小。如果只需要一条指令,则从一个起点生成多个类似的掩码也可以节省空间。

另请参见,对于具有所有相同组件的SSE向量,动态生成还是预计算?它询问了更多关于使用set1内部函数的信息,但不清楚它询问的是常量还是变量的广播。

我还尝试了一些用于广播的编译器输出。

如果缓存未命中是一个问题,请查看您的代码,看看当相同的函数内联到不同的调用方时,编译器是否复制了常量。还要注意一起使用的常量(例如,在一个接一个调用的函数中)是否分散到不同的缓存线中。常数的许多分散加载远比从彼此相邻的位置加载许多常数糟糕。

pmovzx和/或广播加载允许您将更多常量打包到缓存行中,将它们加载到寄存器的开销非常低。负载不会在关键路径上,因此即使需要额外的uop,它也可以在长窗口的任何周期中占用一个空闲的执行单元。

clang实际上在这方面做得很好:不同函数中的独立常量被识别为相同的,这是可以合并相同字符串文本的方式。请注意,clang的asm源输出似乎显示每个函数都有自己的常量副本,但二进制反汇编显示所有这些RIP相对有效地址都引用相同的位置。对于256b版本的重复函数,clang还使用vbroadcastsd来只需要8B的加载,而每个函数都需要额外的指令。(这是在-O3,所以很明显,clang开发人员已经意识到大小对性能很重要,而不仅仅是对操作系统)。IDK为什么不使用vbroadcasts将其降到4B常量,因为这应该同样快。不幸的是,vbroadcast不仅仅来自16B常量的一部分,还使用了其他函数。这可能有道理:某个东西的AVX版本可能只会将它的一些常量与SSE版本合并。最好让内存页中的SSE常量完全处于冷状态,并让AVX版本将其所有常量保持在一起。此外,在组装或链接时处理模式匹配问题也很困难(不管怎样,我并没有阅读每一条指令来确定哪一条指令支持合并)

gcc 5.3也合并常量,但不使用广播加载来压缩32B常量。同样,16B常量与32B常量不重叠。

 类似资料:
  • 问题内容: 我看过这样的例子: 并假设我可以有一个Constants类来包装常量,并声明它们为static final。我几乎不了解Java,并且想知道这是否是创建常量的最佳方法。 问题答案: 这是完全可以接受的,甚至可能是标准。 类型是哪里,是所有大写字母的名称,下划线带有空格,并且VALUE是常量值; 我强烈建议不要将常量放在自己的类或接口中。 附带说明:声明为final且可变的变量仍然可以更

  • 我正在测试REST API。每个API使用不同类型的JSON负载。我不想手动填写所有输入。因此,我希望动态生成JSON(例如,从文本文件读取值并填写JSON结构),然后将生成的JSON作为请求体在API中传递。 最好的方法是什么?对工具或插件有什么建议吗? 附言:嵌套的JSON结构非常复杂。

  • null 另外,如果使用,是否需要导入文件,或者不导入文件,常量将全局可用? 我可以从逻辑上得出一个结论,即是定义自定义错误域之类的东西时的最佳选择(我真的对吗?)。但其他人呢?

  • 问题内容: 如何将两个正则表达式模式构造为一个? 例如,我有一个长模式,一个小模式,我需要在长模式前面放一个小模式。 这行不通。当我连接字符串时,所有的斜杠都消失了。 问题答案: 您必须使用: 当我连接字符串时,所有的斜杠都消失了。 如果您的模式中有反斜杠以转义特殊的正则表达式字符(如),则必须在字符串中使用两个反斜杠(因为是字符串中的转义字符):与相同。 因此,您的模式必须变为:

  • 我对拉雷维尔还不太熟悉。我有一个基本问题,在laravel中添加常量的最佳方法是什么。我知道我们用来添加常量的.env方法。我还制作了一个常量文件,用于我的项目。例如: 等等它最多可以达到100条或更多记录。那么,编写常量的最佳方法应该是什么呢。.env方法。还是添加constant.php文件? 谢啦

  • 事实上中的变量比变量慢,这是众所周知的,并且已经在本网站的不同问题中讨论过了。然而,我仍然没有找到答案的一件事是,使用代码中不同函数内部使用的全局变量(如常量)的最佳和最快的方法是什么? 到目前为止,我能找到的最佳解决方案是定义一个函数,在这里我将全局变量分配给局部变量。 输出: 但这是一个丑陋的解决方案,而且比使用局部变量还要慢。在函数内部使用全局/常量变量等效的最著名的方法是什么,而不必将它们