我正在编写一个例程,用于在BCD(每位十进制4位)和密集包装十进制(DPD)(每3位十进制数位10位)之间进行转换.在Mike Cowlishaw的网站上进一步记录了DPD(建议软件使用查找表).
这个例程只需要它使用的低16位寄存器,但对于较短的指令编码,我尽可能使用32位指令.与代码相关的速度惩罚如下:
mov data,%eax # high 16 bit of data are cleared ... shl %al shr %eax
要么
and $0x888,%edi # = 0000 a000 e000 i000 imul $0x0490,%di # = aei0 0000 0000 0000
其中16位的替代方案imul
是32位imul
,后续and
或一系列lea
指令和最终指令and
.
我的例程中的整个代码可以在下面找到.由于我混合使用单词和双字指令,其中的表现是否有任何可能性更差?
.section .text .type bcd2dpd_mul,@function .globl bcd2dpd_mul # convert BCD to DPD with multiplication tricks # input abcd efgh iklm in edi .align 8 bcd2dpd_mul: mov %edi,%eax # = 0000 abcd efgh iklm shl %al # = 0000 abcd fghi klm0 shr %eax # = 0000 0abc dfgh iklm test $0x880,%edi # fast path for a = e = 0 jz 1f and $0x888,%edi # = 0000 a000 e000 i000 imul $0x0490,%di # = aei0 0000 0000 0000 mov %eax,%esi and $0x66,%esi # q = 0000 0000 0fg0 0kl0 shr $13,%edi # u = 0000 0000 0000 0aei imul tab-8(,%rdi,4),%si # v = q * tab[u-2][0] and $0x397,%eax # r = 0000 00bc d00h 0klm xor %esi,%eax # w = r ^ v or tab-6(,%rdi,4),%ax # x = w | tab[u-2][1] and $0x3ff,%eax # = 0000 00xx xxxx xxxx 1: ret .size bcd2dpd_mul,.-bcd2dpd_mul .section .rodata .align 4 tab: .short 0x0011 ; .short 0x000a .short 0x0000 ; .short 0x004e .short 0x0081 ; .short 0x000c .short 0x0008 ; .short 0x002e .short 0x0081 ; .short 0x000e .short 0x0000 ; .short 0x006e .size tab,.-tab
在从答案和评论以及其他一些技巧中应用一些建议后,这是我改进的代码.
.section .text .type bcd2dpd_mul,@function .globl bcd2dpd_mul # convert BCD to DPD with multiplication tricks # input abcd efgh iklm in edi .align 8 bcd2dpd_mul: mov %edi,%eax # = 0000 abcd efgh iklm shl %al # = 0000 abcd fghi klm0 shr %eax # = 0000 0abc dfgh iklm test $0x880,%edi # fast path for a = e = 0 jnz 1f ret .align 8 1: and $0x888,%edi # = 0000 a000 e000 i000 imul $0x49,%edi # = 0ae0 aei0 ei00 i000 mov %eax,%esi and $0x66,%esi # q = 0000 0000 0fg0 0kl0 shr $8,%edi # = 0000 0000 0ae0 aei0 and $0xe,%edi # = 0000 0000 0000 aei0 mov lookup-4(%rdi),%dx movzbl %dl,%edi imul %edi,%esi # v = q * tab[u-2][0] and $0x397,%eax # r = 0000 00bc d00h 0klm xor %esi,%eax # w = r ^ v or %dh,%al # = w | tab[u-2][1] and $0x3ff,%eax # = 0000 00xx xxxx xxxx ret .size bcd2dpd_mul,.-bcd2dpd_mul .section .rodata .align 4 lookup: .byte 0x11 .byte 0x0a .byte 0x00 .byte 0x4e .byte 0x81 .byte 0x0c .byte 0x08 .byte 0x2e .byte 0x81 .byte 0x0e .byte 0x00 .byte 0x6e .size lookup,.-lookup
Peter Cordes.. 7
TYW用于清楚地评论代码,BTW.它很容易弄清楚发生了什么,以及比特在哪里.我以前从来没有听说过DPD,所以从未注释的代码中解脱出来并且维基百科的文章会被吸引.
相关的问题是:
对于具有立即常量的指令,在Intel CPU上避免使用16位操作数大小.(LCP档位)
在英特尔预IvyBridge上只写低8或16后,避免读取完整的32位或64位寄存器.(部分注册额外的uop).(如果修改像AH这样的upper8 reg,IvB仍然会减速,但Haswell也会删除它).根据Agner Fog的说法,这不仅仅是一个额外的uop:对Core2的惩罚是2至3个周期.我可能测量错了,但在SnB上看起来好坏了.
有关详细信息,请参见http://agner.org/optimize/.
除此之外,使用操作数大小前缀混合一些指令使它们成为16位没有普遍的问题.
您应该将其写为内联asm,而不是作为被调用的函数.您只使用几个寄存器,而快速路径的情况很少是指令.
我看了一下代码.我没有考虑使用明显不同的逻辑来实现相同的结果,只是在优化您拥有的逻辑时.
可能的代码建议:切换分支,以便快速路径具有未采用的分支.实际上,在这种情况下,它可能不会产生任何差异,或者可能会改善慢速路径代码的对齐方式.
.p2align 4,,10 # align to 16, unless we're already in the first 6 bytes of a block of 16 bcd2dpd_mul: mov %edi,%eax # = 0000 abcd efgh iklm shl %al # = 0000 abcd fghi klm0 shr %eax # = 0000 0abc dfgh iklm test $0x880,%edi # fast path for a = e = 0 jnz .Lslow_path ret .p2align 4 # Maybe fine-tune this alignment based on how the rest of the code assembles. .Lslow_path: ... ret
复制返回指令有时比完全最小化代码大小更好.在这种情况下,比较和分支是函数的第4个uop,所以一个被采用的分支不会阻止在第一个时钟周期发出4个uop,并且正确预测的分支仍然会发出返回第二个时钟周期.
imul
对于具有表源的那个,您应该使用32位.(参见下一节关于对齐table
如此读取额外的2B是可以的).在Intel SnB系列微博上,32位imul是一个uop而不是两个uop.由于无法设置符号位,因此low16中的结果应该相同.上面的16被and
前面的最后一个归零ret
,并且不会以任何方式被使用,其中上层16中的垃圾很重要.
imul
,使用直接操作数会产生问题.它在Intel上进行解码时会导致LCP停顿,并且会写入稍后以全宽读取的寄存器的低16位.如果没有屏蔽,它的upper16 将是一个问题(因为它被用作表索引).它的操作数足够大,它们会将垃圾放入上层16,因此需要将其丢弃.
我认为你做这件事的方式对于某些架构来说是最佳的,但事实证明imul r16,r16,imm16
它本身比imul r32,r32,imm32
除了VIA Nano,AMD K7(它比imul32更快)和英特尔P6(从32位/ 64位使用它)的每个架构都要慢模式将LCP失速,并且部分注册减速是一个问题).
在Intel SnB系列CPU上,imul r16,r16,imm16
两个uops,imul32/movzx将严格更好,除了代码大小之外没有任何缺点.在P6系列CPU(即PPro到Nehalem)上,imul r16,r16,imm16
只有一个uop,但那些CPU没有uop缓存,所以LCP停顿可能很关键(除非Nehalem称之为紧密循环,适合28 uop循环缓冲区).对于那些CPU,movzx
从partial-reg失速的角度来看,显式可能更好.Agner Fog说,当CPU插入合并的uop时会有一个额外的循环,这可能意味着一个单独发出额外uop的循环.
在AMD K8-Steamroller上,imul imm16
是2 m-ops而不是1 for imul imm32
,所以imul32/movzx
大约等于imul16
那里.它们不会受到LCP停顿或部分注册问题的影响.
在Intel Silvermont上,imul imm16
是2 uops(每4个时钟吞吐量一个),而imul imm32
不是1 uops(每1个时钟吞吐量一个).在Atom(Silvermont的有序前身)中也是如此:imul16
是一个额外的uop而且速度要慢得多.在大多数其他微体系结构上,吞吐量并不差,只是延迟.
所以,如果你愿意增加代码尺寸以字节为单位它会给出一个加速,你应该使用一个32位imul
和movzwl %di, %edi
.在一些体系,这将是对相同的速度imul imm16
,而在别人这将是多快.AMD推土机系列可能稍微差一点,显然,它不是很擅长同时使用两个整数执行单元,所以EX1的2 m-op指令可能比两个1 m-op指令更好.它们仍然是EX1指令.如果你关心,可以对此进行评分
tab
与至少32B边界对齐,因此您的32位imul
并且or
可以从其中任何2B对齐的条目执行4B加载,而不会跨越缓存行边界.未对齐的访问在所有最近的CPU(Nehalem及更高版本和最近的AMD)上都没有任何损失,只要它们不跨越两个缓存行.
使从表32bit读取的操作避免了Intel CPU具有的部分寄存器损失.AMD CPU和Silvermont不会单独跟踪部分寄存器,因此即使只写低16的指令也必须等待reg的其余部分的结果.这会阻止16位insn打破依赖链.英特尔P6和SnB微阵列系列跟踪部分注册.Haswell完全可以完成双重记账,因为在需要合并时没有任何惩罚,比如在你转移之后,然后转移eax.SnB会在那里插入一个额外的uop,并且在执行此操作时可能会有一个或两个周期的惩罚.我不确定,还没有测试过.但是,我没有看到避免这种情况的好方法.
在shl %al
可以与被替换add %al, %al
.这可以在更多端口上运行.可能没什么区别,因为port0/5(或Haswell及更高版本的端口0/6)可能没有饱和.它们对位具有相同的效果,但设置标志的方式不同.否则它们可以被解码为同一个uop.
变化:分裂PEXT/PDEP /矢量化版本到一个单独的答案,部分因此它可以有自己的意见线程.
TYW用于清楚地评论代码,BTW.它很容易弄清楚发生了什么,以及比特在哪里.我以前从来没有听说过DPD,所以从未注释的代码中解脱出来并且维基百科的文章会被吸引.
相关的问题是:
对于具有立即常量的指令,在Intel CPU上避免使用16位操作数大小.(LCP档位)
在英特尔预IvyBridge上只写低8或16后,避免读取完整的32位或64位寄存器.(部分注册额外的uop).(如果修改像AH这样的upper8 reg,IvB仍然会减速,但Haswell也会删除它).根据Agner Fog的说法,这不仅仅是一个额外的uop:对Core2的惩罚是2至3个周期.我可能测量错了,但在SnB上看起来好坏了.
有关详细信息,请参见http://agner.org/optimize/.
除此之外,使用操作数大小前缀混合一些指令使它们成为16位没有普遍的问题.
您应该将其写为内联asm,而不是作为被调用的函数.您只使用几个寄存器,而快速路径的情况很少是指令.
我看了一下代码.我没有考虑使用明显不同的逻辑来实现相同的结果,只是在优化您拥有的逻辑时.
可能的代码建议:切换分支,以便快速路径具有未采用的分支.实际上,在这种情况下,它可能不会产生任何差异,或者可能会改善慢速路径代码的对齐方式.
.p2align 4,,10 # align to 16, unless we're already in the first 6 bytes of a block of 16 bcd2dpd_mul: mov %edi,%eax # = 0000 abcd efgh iklm shl %al # = 0000 abcd fghi klm0 shr %eax # = 0000 0abc dfgh iklm test $0x880,%edi # fast path for a = e = 0 jnz .Lslow_path ret .p2align 4 # Maybe fine-tune this alignment based on how the rest of the code assembles. .Lslow_path: ... ret
复制返回指令有时比完全最小化代码大小更好.在这种情况下,比较和分支是函数的第4个uop,所以一个被采用的分支不会阻止在第一个时钟周期发出4个uop,并且正确预测的分支仍然会发出返回第二个时钟周期.
imul
对于具有表源的那个,您应该使用32位.(参见下一节关于对齐table
如此读取额外的2B是可以的).在Intel SnB系列微博上,32位imul是一个uop而不是两个uop.由于无法设置符号位,因此low16中的结果应该相同.上面的16被and
前面的最后一个归零ret
,并且不会以任何方式被使用,其中上层16中的垃圾很重要.
imul
,使用直接操作数会产生问题.它在Intel上进行解码时会导致LCP停顿,并且会写入稍后以全宽读取的寄存器的低16位.如果没有屏蔽,它的upper16 将是一个问题(因为它被用作表索引).它的操作数足够大,它们会将垃圾放入上层16,因此需要将其丢弃.
我认为你做这件事的方式对于某些架构来说是最佳的,但事实证明imul r16,r16,imm16
它本身比imul r32,r32,imm32
除了VIA Nano,AMD K7(它比imul32更快)和英特尔P6(从32位/ 64位使用它)的每个架构都要慢模式将LCP失速,并且部分注册减速是一个问题).
在Intel SnB系列CPU上,imul r16,r16,imm16
两个uops,imul32/movzx将严格更好,除了代码大小之外没有任何缺点.在P6系列CPU(即PPro到Nehalem)上,imul r16,r16,imm16
只有一个uop,但那些CPU没有uop缓存,所以LCP停顿可能很关键(除非Nehalem称之为紧密循环,适合28 uop循环缓冲区).对于那些CPU,movzx
从partial-reg失速的角度来看,显式可能更好.Agner Fog说,当CPU插入合并的uop时会有一个额外的循环,这可能意味着一个单独发出额外uop的循环.
在AMD K8-Steamroller上,imul imm16
是2 m-ops而不是1 for imul imm32
,所以imul32/movzx
大约等于imul16
那里.它们不会受到LCP停顿或部分注册问题的影响.
在Intel Silvermont上,imul imm16
是2 uops(每4个时钟吞吐量一个),而imul imm32
不是1 uops(每1个时钟吞吐量一个).在Atom(Silvermont的有序前身)中也是如此:imul16
是一个额外的uop而且速度要慢得多.在大多数其他微体系结构上,吞吐量并不差,只是延迟.
所以,如果你愿意增加代码尺寸以字节为单位它会给出一个加速,你应该使用一个32位imul
和movzwl %di, %edi
.在一些体系,这将是对相同的速度imul imm16
,而在别人这将是多快.AMD推土机系列可能稍微差一点,显然,它不是很擅长同时使用两个整数执行单元,所以EX1的2 m-op指令可能比两个1 m-op指令更好.它们仍然是EX1指令.如果你关心,可以对此进行评分
tab
与至少32B边界对齐,因此您的32位imul
并且or
可以从其中任何2B对齐的条目执行4B加载,而不会跨越缓存行边界.未对齐的访问在所有最近的CPU(Nehalem及更高版本和最近的AMD)上都没有任何损失,只要它们不跨越两个缓存行.
使从表32bit读取的操作避免了Intel CPU具有的部分寄存器损失.AMD CPU和Silvermont不会单独跟踪部分寄存器,因此即使只写低16的指令也必须等待reg的其余部分的结果.这会阻止16位insn打破依赖链.英特尔P6和SnB微阵列系列跟踪部分注册.Haswell完全可以完成双重记账,因为在需要合并时没有任何惩罚,比如在你转移之后,然后转移eax.SnB会在那里插入一个额外的uop,并且在执行此操作时可能会有一个或两个周期的惩罚.我不确定,还没有测试过.但是,我没有看到避免这种情况的好方法.
在shl %al
可以与被替换add %al, %al
.这可以在更多端口上运行.可能没什么区别,因为port0/5(或Haswell及更高版本的端口0/6)可能没有饱和.它们对位具有相同的效果,但设置标志的方式不同.否则它们可以被解码为同一个uop.
变化:分裂PEXT/PDEP /矢量化版本到一个单独的答案,部分因此它可以有自己的意见线程.