Assembly 对大整数进行签名和扩展的最有效的代码是什么?

Assembly 对大整数进行签名和扩展的最有效的代码是什么?,assembly,x86-64,avx,bigint,sign-extension,Assembly,X86 64,Avx,Bigint,Sign Extension,我正在用x86-64汇编语言编写一个代码库,为s0128,s0256,s0512,s1024,s2048,和s4096有符号整数类型和f0128,f0256,提供所有常规的按位、移位、逻辑、比较、算术和数学函数,f0512,f1024,f2048,以及f4096浮点类型 现在,我正在编写一些类型转换例程,遇到了一些应该很简单但需要的指令比我预期的多得多的东西。我觉得我一定错过了一些让这更容易的东西(一些说明),但到目前为止没有运气 s0256结果的低位128位仅仅是s0128输入参数的副本,而s

我正在用x86-64汇编语言编写一个代码库,为
s0128
s0256
s0512
s1024
s2048
,和
s4096
有符号整数类型和
f0128
f0256
,提供所有常规的按位、移位、逻辑、比较、算术和数学函数,
f0512
f1024
f2048
,以及
f4096
浮点类型

现在,我正在编写一些类型转换例程,遇到了一些应该很简单但需要的指令比我预期的多得多的东西。我觉得我一定错过了一些让这更容易的东西(一些说明),但到目前为止没有运气

s0256
结果的低位128位仅仅是
s0128
输入参数的副本,而
s0256
结果的高位128位中的所有位必须设置为
s0128
输入参数中的最高有效位

简单,嗯?但这里是迄今为止我能想到的将
s0256
转换为
s0128
的最佳方法。忽略前4行(它们只是参数错误检查)和最后2行(从函数返回时没有错误(rax==0))。中间的5条线是所讨论的算法。尽量避免[有条件的]跳转指令

.text
.align 64
big_m63:
.quad  -63, -63                       # two shift counts for vpshaq instruction

big_s0256_eq_s0128:    # (s0256* arg0, const s0128* arg1); # s0256 = s0256(s0128)
  orq        %rdi, %rdi               # is arg0 a valid address ???
  jz         error_argument_invalid   # nope
  orq        %rsi, %rsi               # is arg1 a valid address ???
  jz         error_argument_invalid   # nope

  vmovapd    (%rsi), %xmm0            # ymm0 = arg1.ls64 : arg1.ms64 : 0 : 0
  vmovhlps   %xmm0, %xmm0, %xmm1      # ymm1 = arg1.ms64 : arg1.ms64 : 0 : 0
  vpshaq     big_m63, %xmm1, %xmm1    # ymm1 = arg1.sign : arg1.sign : 0 : 0
  vperm2f128 $32, %ymm1, %ymm0, %ymm0 # ymm1 = arg1.ls64 : arg1.ms64 : sign : sign
  vmovapd    %ymm0, (%rdi)            # arg0 = arg1 (sign-extended to 256-bits)

  xorq       %rax, %rax               # rax = 0 == no error
  ret                                 # return from function
此例程也是非最优的,因为每条指令都需要前一条指令的结果,这会阻止任何指令的并行执行

.text
.align 64
big_m63:
.quad  -63, -63                       # two shift counts for vpshaq instruction

big_s0256_eq_s0128:    # (s0256* arg0, const s0128* arg1); # s0256 = s0256(s0128)
  orq        %rdi, %rdi               # is arg0 a valid address ???
  jz         error_argument_invalid   # nope
  orq        %rsi, %rsi               # is arg1 a valid address ???
  jz         error_argument_invalid   # nope

  vmovapd    (%rsi), %xmm0            # ymm0 = arg1.ls64 : arg1.ms64 : 0 : 0
  vmovhlps   %xmm0, %xmm0, %xmm1      # ymm1 = arg1.ms64 : arg1.ms64 : 0 : 0
  vpshaq     big_m63, %xmm1, %xmm1    # ymm1 = arg1.sign : arg1.sign : 0 : 0
  vperm2f128 $32, %ymm1, %ymm0, %ymm0 # ymm1 = arg1.ls64 : arg1.ms64 : sign : sign
  vmovapd    %ymm0, (%rdi)            # arg0 = arg1 (sign-extended to 256-bits)

  xorq       %rax, %rax               # rax = 0 == no error
  ret                                 # return from function
是否有更好的指示右移标志扩展?我找不到像
vpshaq
这样接受立即字节来指定移位计数的指令,尽管我不知道为什么(许多SIMD指令具有用于各种目的的立即8位操作数)。此外,英特尔不支持
vpshaq
。哎呀

但是你看!StephenCanon对下面这个问题有一个绝妙的解决方案!令人惊叹的!该解决方案比上述多了一条指令,但
vpxor
指令可以放在第一条
vmovapd
指令之后,并且实际所用的周期不应超过上述5条指令版本。好极了

为了完整性和便于比较,以下是带有最新Stephencon增强功能的代码:

.text
.align 64
big_s0256_eq_s0128:    # (s0256* arg0, const s0128* arg1); # s0256 = s0256(s0128)
  orq        %rdi, %rdi               # is arg0 a valid address ???
  jz         error_argument_invalid   # nope
  orq        %rsi, %rsi               # is arg1 a valid address ???
  jz         error_argument_invalid   # nope

  vmovapd    (%rsi), %xmm0            # ymm0 = arg1.ls64 : arg1.ms64 : 0 : 0
  vpxor      %xmm2, %xmm2, %xmm2      # ymm2 = 0 : 0 : 0 : 0
  vmovhlps   %xmm0, %xmm0, %xmm1      # ymm1 = arg1.ms64 : arg1.ms64 : 0 : 0
  vpcmpgtq   %xmm1, %xmm2, %xmm1      # ymm1 = arg1.sign : arg1.sign : 0 : 0
  vperm2f128 $32, %ymm1, %ymm0, %ymm0 # ymm1 = arg1.ls64 : arg1.ms64 : sign : sign
  vmovapd    %ymm0, (%rdi)            # arg0 = arg1 (sign-extended to 256-bits)

  xorq       %rax, %rax               # rax = 0 == no error
  ret                                 # return from function

我不确定,但不需要从内存中读取这两个64位移位计数也可能会稍微加快代码的速度。很好。

你把事情复杂化了。登录
rax
后,只需从那里执行两个64b存储,而不是尝试在
ymm0
中组合结果。少了一条指令和更短的依赖链

当然,随着目的地类型变大,使用更广泛的存储(AVX)是有意义的。使用AVX2,您可以使用
vbroadcastq
更有效地进行splat,但看起来您的目标是基线AVX

我还应该注意到,一旦你得到了~512b个整数,对于大多数算法来说,像乘法这样的超线性运算的代价完全支配着运行时间,以至于压缩像符号扩展这样的运算的每一个最后周期都会很快开始失去价值。这是一个很好的练习,但一旦您的实现“足够好”,它最终并不是最有效地利用您的时间


经过进一步考虑,我有以下建议:

vmovhlps  %xmm0, %xmm0, %xmm1 // could use a permute instead to stay in integer domain.
vpxor     %xmm2, %xmm2, %xmm2
vpcmpgtq  %xmm1, %xmm2, %xmm2 // generate sign-extension without shift
这具有以下优点:(a)不需要恒定负载;(b)在Intel和AMD上都能工作。生成零的xor看起来像是一条额外的指令,但实际上,这种零化习惯用法在最近的处理器上甚至不需要执行槽


FWIW,如果目标是AVX2,我可能会这样写:

vmovdqa (%rsi),        %xmm0 // { x0, x1, 0,  0  }
vpermq   $0x5f, %ymm0, %ymm1 // { 0,  0,  x1, x1 }
vpxor    %ymm2, %ymm2, %ymm2 // { 0,  0,  0,  0  }
vpcmpgtq %ymm1, %ymm2, %ymm2 // { 0,  0,  s,  s  } s = sign extension
vpor     %ymm2, %ymm0, %ymm0 // { x0, x1, s,  s  }
vmovdqa  %ymm0,       (%rdi)

不幸的是,我不认为
vpermq
在AMD上可用。

我不知道
AVX2
还可用。我必须测试的CPU是
FX8150
FX8350
。我愿意采用这些CPU的任何指令。是的,我支持的数据类型是
s0128
s0256
s0512
s1024
s2048
s4096
整数和
f0128
f0256
f0512
f1024
f2048
f4096
浮点数。对于较大的尺寸,最好的方法会随着用符号位填充的64位块数变为la而变化rge。这就是为什么我尝试在
SIMD
ymm
寄存器中尽可能多地执行操作的原因,希望这对于较大的数据类型更有效。@honestann:对,对于大数据类型,您将希望使用AVX来显示符号位,如您的示例所示。对于128->256的特定情况(您碰巧在这里使用)只需存储GPRs数据就可以了。Intel“Haswell”中提供了AVX2“微体系结构,但在AMD部件中还不可用,所以它不是您的选择。@StevenCanon:另外,我有点担心在CPU中重复写入不同的值到缓存的单个64字节行,从而导致缓存电路的麻烦和延迟。由于必须进行所有合并,我担心将单个缓存线的比特和片段吐到缓存(特别是在连续指令上)可能会迫使缓存电路施加额外的延迟。我讨厌成为一个可怜的混蛋,他必须设计缓存电路来处理这种情况!大多数缓存非常擅长处理相邻的64b存储。在某些体系结构上,如果您发布重叠存储,确实会遇到麻烦,您确实需要小心存储数据,然后在访问大小不同时立即重新加载数据,但对于您在此处的使用,你会很安全。每个车道的换班量需要出现在换班计数向量的相应车道中。因此,如果您想在不首先执行
movhlps
的情况下进行移位,则需要使用{x,63}作为移位计数