热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:使用SSE计算绝对值的最快方法

篇首语:本文由编程笔记#小编为大家整理,主要介绍了使用SSE计算绝对值的最快方法相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了使用SSE计算绝对值的最快方法相关的知识,希望对你有一定的参考价值。



我知道3种方法,但据我所知,通常只使用前2种方法:



  1. 使用andpsandnotps掩盖符号位。
    优点:一个快速指令,如果掩码已经在寄存器中,这使得它非常适合在循环中多次执行此操作。
    缺点:掩码可能不在寄存器中或更糟糕,甚至不在缓存中,导致非常长的内存提取。

  2. 将值从零减去否定,然后得到原始的最大值并取消。
    优点:固定成本,因为无需取物,就像面具一样。
    缺点:如果条件理想,将始终比掩码方法慢,并且我们必须等待subps完成才能使用maxps指令。

  3. 与选项2类似,将原始值从零减去否定,然后使用andps将结果与“原位”和“原点”相加。我运行了一个测试,将其与方法2进行比较,除了处理NaNs时,它似乎与方法2的行为相同,在这种情况下,结果将是与方法2的结果不同的NaN
    优点:应该比方法2略快,因为andps通常比maxps快。
    缺点:当涉及到NaNs时,这会导致任何意外行为吗?也许不是,因为NaN仍然是NaN,即使它是NaN的不同值,对吧?

欢迎提出想法和意见。


答案

TL; DR:几乎在所有情况下,使用pcmpeq / shift生成掩码,并使用它来安装。它具有迄今为止最短的关键路径(与内存中的常量相关联),并且不能缓存未命中。


How to do that with intrinsics

让编译器在未初始化的寄存器上发出pcmpeqd可能很棘手。 (godbolt)。 gcc / icc的最佳方式就是

__m128 abs_mask(void){
// with clang, this turns into a 16B load,
// with every calling function getting its own copy of the mask
__m128i minus1 = _mm_set1_epi32(-1);
return _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));
}
// MSVC is BAD when inlining this into loops
__m128 vecabs_and(__m128 v) {
return _mm_and_ps(abs_mask(), v);
}
__m128 sumabs(const __m128 *a) { // quick and dirty no alignment checks
__m128 sum = vecabs_and(*a);
for (int i=1 ; i <10000 ; i++) {
// gcc, clang, and icc hoist the mask setup out of the loop after inlining
// MSVC doesn't!
sum = _mm_add_ps(sum, vecabs_and(a[i])); // one accumulator makes addps latency the bottleneck, not throughput
}
return sum;
}

clang 3.5及更高版本“优化”set1 / shift以从内存加载常量。不过,它将使用pcmpeqd来实现set1_epi32(-1)。 TODO:找到一系列内在函数,用clang生成所需的机器代码。从内存加载常量不是性能灾难,但让每个函数使用不同的掩码副本是非常可怕的。

MSVC:VS2013:



  • _mm_uninitialized_si128()没有定义。

  • 对于未初始化的变量,_mm_cmpeq_epi32(self,self)将在此测试用例中发出一个movdqa xmm, [ebp-10h](即从堆栈中加载一些未初始化的数据。这样可以减少缓存未命中的风险,而不仅仅是从内存加载最终常量。但是,Kumputer说MSVC没有设法将pcmpeqd / psrld提升出循环(我假设在内联vecabs时),所以这是不可用的,除非你手动内联并自行提升循环中的常量。

  • 使用_mm_srli_epi32(_mm_set1_epi32(-1), 1)导致movdqa加载所有-1的向量(在循环外悬挂),并在循环内加载psrld。所以这太可怕了。如果您要加载16B常量,它应该是最终的向量。每个循环迭代生成掩码的整数指令也很可怕。

对MSVC的建议:放弃动态生成掩码,然后写

const __m128 absmask = _mm_castsi128_ps(_mm_set1_epi32(~(1<<31));

可能你只是将掩码存储在内存中作为16B常量。希望不会为使用它的每个功能重复。将掩码放在内存常量中更有可能在32位代码中有用,在32位代码中你只有8个XMM寄存器,所以vecabs可以只使用内存源操作数进行ANDPS,如果它没有一个可以保持常量的寄存器。

TODO:找出如何避免在内联的每个地方重复常量。可能使用全局常量,而不是匿名的set1,会很好。但是你需要初始化它,但我不确定内在函数是否作为全局__m128变量的初始化器。您希望它进入只读数据部分,而不是在程序启动时运行的构造函数。



或者,使用

__m128i minus1; // undefined
#if _MSC_VER && !__INTEL_COMPILER
minus1 = _mm_setzero_si128(); // PXOR is cheaper than MSVC's silly load from the stack
#endif
minus1 = _mm_cmpeq_epi32(minus1, minus1); // or use some other variable here, which will probably cost a mov insn without AVX, unless the variable is dead.
const __m128 absmask = _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));

额外的PXOR非常便宜,但它仍然是一个uop,代码大小仍然是4个字节。如果有人有任何更好的解决方案来克服MSVC不愿意发出我们想要的代码,请留下评论或编辑。但是,如果内联到循环中,这是不好的,因为pxor / pcmp / psrl都将在循环内部。

使用movd加载32位常数并使用shufps进行广播可能没问题(同样,你可能不得不手动将其从循环中提升)。这是3个指令(对于GP reg,movd,shufps是mov-immediate),而且在两个整数核心之间共享向量单元的AMD上,movd很慢。 (他们的超线程版本。)




选择最好的asm序列

好吧,让我们看看这个,让我们通过Skylake说英特尔Sandybridge,稍微提一下Nehalem。请参阅Agner Fog's微型指南和指导时间,了解我如何解决这个问题。我还使用了在http://realwordtech.com/论坛上发帖链接的Skylake号码。



让我们说我们想要abs()的向量是在xmm0中,并且是FP代码典型的长依赖链的一部分。

因此,假设任何不依赖于xmm0的操作都可以在xmm0准备好之前开始几个循环。我已经测试过,并且内存操作数的指令不会给依赖链增加额外的延迟,假设内存操作数的地址不是dep链的一部分(即不是关键路径的一部分)。



我不完全清楚记忆操作在它是微融合uop的一部分时的早期开始。根据我的理解,重新排序缓冲区(ROB)与融合的uops一起工作,并跟踪从发布到退役(168(SnB)到224(SKL)条目)的uops。还有一个在未融合域中工作的调度程序,只保留已准备好但尚未执行的输入操作数的微指令。 uops可以在解码(或从uop缓存加载)的同时发出到ROB(融合)和调度程序(unfused)。 If I'm understanding this correctly, it's 54 to 64 entries in Sandybridge to Broadwell和Skylake的97。 There's some unfounded speculation about it not being a unified (ALU/load-store) scheduler anymore。

还有人谈到Skylake每时钟处理6次uop。据我所知,Skylake会将每个时钟的整个uop-cache行(最多6个uop)读入uop缓存和ROB之间的缓冲区。进入ROB /调度程序的问题仍然是4个问题。 (甚至nop仍然是每时钟4)。这个缓冲区有助于code alignment / uop cache line boundaries在以前的Sandybridge-microarch设计中造成瓶颈。我以前认为这个“问题队列”是这个缓冲区,但显然它不是。

但是,如果地址不在关键路径上,则调度程序足够大,可以及时准备好缓存中的数据。




1a: mask with a memory operand

ANDPS xmm0, [mask] # in the loop


  • bytes:7 insn,16 data。 (AVX:8 insn)

  • 融合域uops:1 * n

  • 延迟添加到关键路径:1c(假设L1缓存命中)

  • 吞吐量:1 / c。 (Skylake: 2/c)(限制为2次/ c)

  • 如果xmm0在这个insn发布时准备好了“延迟”:在L1缓存命中时~4c。




1b: mask from a register

movaps xmm5, [mask] # outside the loop
ANDPS xmm0, xmm5 # in a loop
# or PAND xmm0, xmm5 # higher latency, but more throughput on Nehalem to Broadwell
# or with an inverted mask, if set1_epi32(0x80000000) is useful for something else in your loop:
VANDNPS xmm0, xmm5, xmm0 # It's the dest that's NOTted, so non-AVX would need an extra movaps


  • bytes:10 insn + 16 data。 (AVX:12个insn字节)

  • 融合域uops:1 + 1 * n

  • 延迟添加到dep链:1c(在循环的早期具有相同的cache-miss警告)

  • 吞吐量:1 / c。 (Skylake: 3/c)

PAND在Nehalem到Broadwell的吞吐量是3 / c,但是延迟= 3c(如果在两个FP域操作之间使用,在Nehalem上更糟)。我猜只有port5具有将按位运算直接转发到其他FP执行单元(前Skylake)的接线。 Pre-Nehalem,在AMD上,按位FP操作与整数FP操作相同,因此它们可以在所有端口上运行,但具有转发延迟。




1c: generate the mask on the fly:

# outside a loop
PCMPEQD xmm5, xmm5 # set to 0xff... Recognized as independent of the old value of xmm5, but still takes an execution port (p1/p5).
PSRLD xmm5, 1 # 0x7fff... # port0
# or PSLLD xmm5, 31 # 0x8000... to set up for ANDNPS
ANDPS xmm0, xmm5 # in the loop. # port5


  • 字节:12(AVX:13)

  • 融合域uops:2 + 1 * n(无内存操作)

  • 延迟添加到dep链:1c

  • 吞吐量:1 / c。 (Skylake: 3/c)

  • 所有3个uop的吞吐量:1 / c使所有3个向量ALU端口饱和

  • “延迟”,如果xmm0在此序列发出时准备就绪(无循环):3c(如果ANDPS必须等待整数数据准备好,则SnB / IvB上可能有1c旁路延迟.Agner Fog说在某些情况下没有额外的延迟整数 - > SnB / IvB上的FP-boolean。)

此版本的内存仍然比内存中具有16B常量的版本少。它也适用于不经常调用的函数,因为没有负载会导致缓存未命中。

“旁路延迟”应该不是问题。如果xmm0是长依赖链的一部分,则掩码生成指令将提前执行​​,因此xmm5中的整数结果将有时间在xmm0准备好之前达到ANDPS,即使它采用慢速通道。

根据Agner Fog的测试,Haswell没有整数结果的旁路延迟 - > FP boolean。他对SnB / IvB的描述说这是一些整数指令输出的情况。因此,即使在这个指令序列发出时xmm0准备就绪的“站立开始”开始的de-chain链情况下,它只有3c on * well,4c on * Bridge。如果执行单元正在清除积压的uop,那么延迟可能无关紧要。

无论哪种方式,ANDPS的输出将在FP域中,并且如果在MULPS或其他东西中使用,则没有旁路延迟。

在Nehalem,绕行延误是2c。所以在Nehalem的dep链开始时(例如在分支错误预测或I $ miss之后),如果xmm0在发出此序列时准备就绪,那么“延迟”为5c。如果你非常关心Nehalem,并且期望这个代码成为频繁的分支错误预测或类似的管道停顿之后运行的第一件事,这使得OoOE机器无法在xmm0准备好之前开始计算掩码,那么这可能不是非循环情况的最佳选择。




2a: AVX max(x, 0-x)

VXORPS xmm5, xmm5, xmm5 # outside the loop
VSUBPS xmm1, xmm5, xmm0 # inside the loop
VMAXPS xmm0, xmm0, xmm1


  • 字节:AVX:12

  • 融合域uops:1 + 2 * n(无内存操作)

  • 延迟添加到dep链:6c(Skylake:8c)

  • 吞吐量:每2c 1个(两个port1 uops)。 (Skylake:1 / c,假设MAXPS使用与SUBPS相同的两个端口。)

Skylake删除了单独的vector-FP add单元,并在端口0和1上的FMA单元中添加了向量。这使FP增加了一倍的吞吐量,代价是延迟1c。 FMA latency is down to 4 (from 5 in *well)。 x87 FADD仍然是3周期延迟,所以仍然有一个3周期标量80bit-FP加法器,但只在一个端口上。


2b: same but without AVX:

# insi

推荐阅读
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了C++中省略号类型和参数个数不确定函数参数的使用方法,并提供了一个范例。通过宏定义的方式,可以方便地处理不定参数的情况。文章中给出了具体的代码实现,并对代码进行了解释和说明。这对于需要处理不定参数的情况的程序员来说,是一个很有用的参考资料。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 判断数组是否全为0_连续子数组的最大和的解题思路及代码方法一_动态规划
    本文介绍了判断数组是否全为0以及求解连续子数组的最大和的解题思路及代码方法一,即动态规划。通过动态规划的方法,可以找出连续子数组的最大和,具体思路是尽量选择正数的部分,遇到负数则不选择进去,遇到正数则保留并继续考察。本文给出了状态定义和状态转移方程,并提供了具体的代码实现。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • This article discusses the efficiency of using char str[] and char *str and whether there is any reason to prefer one over the other. It explains the difference between the two and provides an example to illustrate their usage. ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
author-avatar
lan1998_789
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有