如何在一个8位字节中对四个2位位域求和?

时间:2013-07-26 11:29:44

标签: c optimization assembly bit-manipulation bit-fields

我有四个2位位字节存储在一个字节中。因此,每个位域可以表示0,1,2或3.例如,以下是前3个位域为零的4个可能值:

00 00 00 00 = 0 0 0 0
00 00 00 01 = 0 0 0 1
00 00 00 10 = 0 0 0 2
00 00 00 11 = 0 0 0 3

我想要一种有效的方法来对四个位域进行求和。例如:

11 10 01 00 = 3 + 2 + 1 + 0 = 6

现代Intel x64 CPU上的8位查找表需要4个周期才能从L1返回答案。似乎应该有一些方法来比这更快地计算答案。 3个周期为6-12个简单位操作提供了空间。作为首发,简单的面具和换挡在Sandy Bridge上需要5个周期:

假设位字段为:d c b a,并且该掩码为:00 00 00 11

在Ira的帮助下澄清:这假设abcd是相同的,并且都已设置为初始{{1} }。奇怪的是,我想我可以免费做到这一点。由于我每个周期可以执行2次加载,而不是加载byte一次,我只需加载四次:bytea在第一个周期db {1}}在第二个。后两个负载将延迟一个周期,但直到第二个周期我才需要它们。下面的分裂代表了事情应该如何分解成不同的循环。

c

对于使逻辑更容易的位域的不同编码实际上是好的,只要它适合单个字节并且以某种方式与该方案一对一地映射。下降到装配也很好。目前的目标是Sandy Bridge,但针对Haswell或更远的目标也很好。

应用程序和动机:我正在尝试使开源变量位解压缩例程更快。每个位域表示随后的四个整数中的每一个的压缩长度。我需要总和来知道我需要多少字节才能跳到下一组四个字节。当前循环需要10个周期,其中5个是我试图避免的查找。削减一个周期将提高约10%。

编辑:最初我说“8个周期”,但正如Evgeny指出的那样,我错了。正如Evgeny所指出的那样,只有在没有使用索引寄存器的情况下从系统内存的前2K加载时,才会出现间接4周期加载。可以在Intel Architecture Optimization Manual第2.12节

中找到正确的延迟列表
a = *byte
d = *byte

b = *byte
c = *byte

latency

latency

a &= mask
d >>= 6

b >>= 2
c >>= 4
a += d

b &= mask
c &= mask

b += c

a += b

编辑:我认为这是Ira的解决方案如何进入周期。我认为加载后也需要5个工作周期。

>    Data Type       (Base + Offset) > 2048   (Base + Offset) < 2048 
>                     Base + Index [+ Offset]
>     Integer                5 cycles               4 cycles
>     MMX, SSE, 128-bit AVX  6 cycles               5 cycles
>     X87                    7 cycles               6 cycles 
>     256-bit AVX            7 cycles               7 cycles

6 个答案:

答案 0 :(得分:5)

内置的POPCOUNT指令会有帮助吗?

n = POPCOUNT(byte&0x55);
n+= 2*POPCOUNT(byte&0xAA)

或者

  word = byte + ((byte&0xAA) << 8);
  n = POPCOUNT(word);

不确定总时间。 This discussion表示popcount有3个周期延迟,1个吞吐量。


更新:
我可能遗漏了一些关于如何运行IACA的重要事实,但经过12-11吞吐量范围的一些实验后,我编译了以下内容:

 uint32_t decodeFast(uint8_t *in, size_t count) {
  uint64_t key1 = *in;
  uint64_t key2;
  size_t adv;
  while (count--){
     IACA_START;
     key2=key1&0xAA;
     in+= __builtin_popcount(key1);
     adv= __builtin_popcount(key2);
     in+=adv+4;
     key1=*in;
  }
  IACA_END;
  return key1;
}

gcc -std=c99 -msse4 -m64 -O3 test.c

并且 3.55 周期!?!:

Block Throughput: 3.55 Cycles       Throughput Bottleneck: InterIteration
|  Uops  |  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
---------------------------------------------------------------------
|   1    |           | 1.0 |           |           |     |     |    | popcnt edx,eax
|   1    | 0.9       |     |           |           |     | 0.1 | CP | and eax,0x55 
|   1    |           | 1.0 |           |           |     |     | CP | popcnt eax,eax
|   1    | 0.8       |     |           |           |     | 0.2 |    | movsxd rdx,edx
|   1    | 0.6       |     |           |           |     | 0.4 |    | add rdi, rdx
|   1    | 0.1       | 0.1 |           |           |     | 0.9 | CP | cdqe 
|   1    | 0.2       | 0.3 |           |           |     | 0.6 |    | sub rsi, 1
|   1    | 0.2       | 0.8 |           |           |     |     | CP | lea rdi,[rdi+rax+4] 
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     | CP | movzx eax,[rdi]
|   1    |           |     |           |           |     | 1.0 |    | jnz 0xffff

另外两个想法

可能的微优化以在2条指令中进行求和

total=0;
PDEP(vals,0x03030303,*in);  #expands the niblets into bytes
PSADBW(total,vals) #total:= sum of abs(0-byte) for each byte in vals

每个的延迟应该是3,所以这可能没有帮助。也许可以用简单的移位替换逐字节求和,并沿着AX=total+total>>16; ADD AL,AH

的行添加

宏观优化:
您提到使用密钥作为对随机指令表的查找。为什么不直接将距离存储到下一个键以及随机指令?要么存储一个更大的表,要么可能将4位长度压缩到随机密钥的未使用位3-6,代价是需要一个掩码来提取它。

答案 1 :(得分:4)

考虑

 temp = (byte & 0x33) + ((byte >> 2) & 0x33);
 sum = (temp &7) + (temp>>4);

应该是9台机器指令,其中许多是并行执行的。 (OP的第一次尝试是 9条指令加上一些未提及的动作。)

在检查时,这似乎有太多的序列依赖关系 是一场胜利。

编辑:关于二元操作具有破坏性的讨论,LEA避免这种情况, 让我思考如何使用LEA组合多个操作数, 并乘以常数。上面的代码尝试正确规范化 通过向右移动得到答案,但我们可以通过乘法使答案正常化。 有了这种洞察力,这段代码可能有用:

     mov     ebx,  byte      ; ~1: gotta start somewhere
     mov     ecx, ebx        ; ~2: = byte
     and     ebx, 0xCC       ; ~3: 2 sets of 2 bits, with zeroed holes
     and     ecx, 0x33       ; ~3: complementary pair of bits
     lea     edx, [ebx+4*ecx] ; ~4: sum bit pairs, forming 2 4-bit sums
     lea     edi, [8*edx+edx] ; ~5: need 16*(lower bits of edx)
     lea     edi, [8*edx+edi] ; ~6: edi = upper nibble + 16* lower nibble
     shr     edi, 4           ; ~7: right normalized
     and     edi, 0x0F        ; ~8: masked

嗯,娱乐性但仍然没有成功。 3个时钟不是很长: - {

答案 2 :(得分:4)

其他答案提出了各种方法来将值放在单个变量中(不解包它们)。虽然这些方法提供了相当好的吞吐量(特别是POPCNT),但它们具有很大的延迟 - 要么是因为计算链很长,要么是因为使用了高延迟指令。

使用正常的加法指令(一次加一对值)可能会更好,使用掩码和移位之类的简单操作将这些值彼此分开,并使用指令级并行来有效地执行此操作。对于使用单个64位寄存器而不是内存的表查找变体的字节提示中的两个中间值的位置。所有这些都可以加快四个总和的计算速度,并且只使用4或5个时钟。

OP中建议的原始表查找方法可能包括以下步骤:

  1. 使用内存中的四个值加载字节(5个时钟)
  2. 使用查找表(5个时钟)
  3. 计算值的总和
  4. 更新指针(1个时钟)
  5. 64字节寄存器查找

    以下代码段显示了如何在5个时钟中执行步骤#2,并结合步骤#2和#3,保持延迟仍为5个时钟(可针对内存负载使用复杂寻址模式优化为4个时钟):

    p += 5 + (*p & 3) + (*p >> 6) +
      ((0x6543543243213210ull >> (*p & 0x3C)) & 0xF);
    

    这里常数&#34; 5&#34;意味着我们跳过具有长度的当前字节以及对应于全零长度的4个数据字节。此代码段对应于以下代码(仅限64位):

    mov eax, 3Ch
    and eax, ebx              ;clock 1
    mov ecx, 3
    and ecx, ebx              ;clock 1
    shr ebx, 6                ;clock 1
    add ebx, ecx              ;clock 2
    mov rcx, 6543543243213210h
    shr rcx, eax              ;clock 2..3
    and ecx, Fh               ;clock 4
    add rsi, 5
    add rsi, rbx              ;clock 3 or 4
    movzx ebx, [rsi + rcx]    ;clock 5..9
    add rsi, rcx
    

    我尝试使用以下编译器自动生成此代码:gcc 4.6.3,clang 3.0,icc 12.1.0。前两个人没有做任何好事。但是英特尔的编译器几乎完美地完成了这项工作。


    使用ROR指令进行快速位域提取

    编辑:Nathan的测试显示以下方法存在问题。 Sandy Bridge上的ROR指令使用两个端口并与SHR指令冲突。所以这个代码在Sandy Bridge上需要多一个时钟,这使它不是很有用。可能它会在Ivy Bridge和Haswell上按预期工作。

    没有必要将64位寄存器的技巧用作查找表。相反,您可以将字节旋转4位,将两个中间值放在第一个和第四个值的位置。然后你可以用同样的方式处理它们。这种方法至少有一个缺点。在C中表示字节旋转并不容易。此外,我不太确定这种旋转,因为在较旧的处理器上,它可能导致部分寄存器停顿。优化手动提示,对于Sandy Bridge,如果操作源与目标相同,我们可以更新部分寄存器,而不会停止。但我不确定我是否理解得当。我没有适当的硬件来检查这个。无论如何,这是代码(现在它可能是32位或64位):

    mov ecx, 3
    and ecx, ebx              ;clock 1
    shr ebx, 6                ;clock 1
    add ebx, ecx              ;clock 2
    ror al, 4                 ;clock 1
    mov ecx, 3
    and ecx, eax              ;clock 2
    shr eax, 6                ;clock 2
    add eax, ecx              ;clock 3
    add esi, 5
    add esi, ebx              ;clock 3
    movzx ebx, [esi+eax]      ;clocks 4 .. 8
    movzx eax, [esi+eax]      ;clocks 4 .. 8
    add esi, eax
    

    使用AL和AH之间的边界来解压缩位域

    此方法与前一个方法的区别仅在于提取两个中间位域的方式。而不是在Sandy Bridge上昂贵的ROR,使用简单的换档。该移位将寄存器AL中的第二位域和第三位域定位在AH中。然后用移位/掩码提取它们。与之前的方法一样,这里存在部分寄存器停顿的可能性,现在是两个指令而不是一个。但很有可能Sandy Bridge和更新的处理器可以毫不拖延地执行它们。

    mov ecx, 3
    and ecx, ebx              ;clock 1
    shr ebx, 6                ;clock 1
    add ebx, ecx              ;clock 2
    shl eax, 4                ;clock 1
    mov edx, 3
    and dl, ah                ;clock 2
    shr al, 6                 ;clock 2
    add dl, al                ;clock 3
    add esi, 5
    add esi, ebx              ;clock 3
    movzx ebx, [esi+edx]      ;clock 4..8
    movzx eax, [esi+edx]      ;clock 4..8
    add esi, edx
    

    并行加载和计算总和

    此外,无需加载4个长度的字节并按顺序计算总和。您可以并行执行所有这些操作。四个总和只有13个值。如果您的数据是可压缩的,那么您很少会看到此总和大于7.这意味着您可以将前8个最可能的字节加载到64位寄存器,而不是加载单个字节。而且你可以比计算四者的总和更早。计算总和时加载8个值。然后你就可以通过shift和mask从这个寄存器中获得适当的值。该想法可以与用于计算总和的任何手段一起使用。这里使用简单的表查找:

    typedef unsigned long long ull;
    ull four_lengths = *p;
    for (...)
    {
      ull preload = *((ull*)(p + 5));
      unsigned sum = table[four_lengths];
      p += 5 + sum;
    
      if (sum > 7)
        four_lengths = *p;
      else
        four_lengths = (preload >> (sum*8)) & 15;
    }
    

    使用适当的汇编代码,这只会延迟2个时钟:shift和mask。它提供7个时钟(但仅限于可压缩数据)。

    如果将表查找更改为计算,则可能只有6个时钟的循环延迟:4将值加在一起并更新指针,2表示移位和掩码。有趣的是,在这种情况下,循环延迟仅由计算确定,并且不依赖于内存负载的延迟。


    并行加载和计算总和(确定性方法)

    可以以确定的方式并行执行负载和求和。加载两个64位寄存器,然后使用CMP + CMOV选择其中一个寄存器是一种可能性,但它不会提高顺序计算的性能。其他可能性是使用128位寄存器和AVX。在128位寄存器和GPR /内存之间迁移数据会增加大量延迟(但如果我们每次迭代处理两个数据块,则可以消除一半的延迟)。此外,我们还需要对AVX寄存器使用字节对齐的内存加载(这也会增加循环延迟)。

    这个想法是在AVX中执行所有计算,除了应该从GPR完成的内存加载。 (有一种替代方法可以在AVX中完成所有操作并在Haswell上使用broadcast + add + gather,但它不太可能更快)。另外,将数据加载到一对AVX寄存器(每次迭代处理两个数据块)应该是有用的。这允许成对的加载操作部分重叠并消除一半的额外延迟。

    首先从寄存器中解压缩正确的字节:

    vpshufb xmm0, xmm6, xmm0      ; clock 1
    

    将四个位域加在一起:

    vpand xmm1, xmm0, [mask_12]   ; clock 2 -- bitfields 1,2 ready
    vpand xmm2, xmm0, [mask_34]   ; clock 2 -- bitfields 3,4 (shifted)
    vpsrlq xmm2, xmm2, 4          ; clock 3 -- bitfields 3,4 ready
    vpshufb xmm1, xmm5, xmm1      ; clock 3 -- sum of bitfields 1 and 2
    vpshufb xmm2, xmm5, xmm2      ; clock 4 -- sum of bitfields 3 and 4
    vpaddb xmm0, xmm1, xmm2       ; clock 5 -- sum of all bitfields
    

    然后更新地址并加载下一个字节向量:

    vpaddd xmm4, xmm4, [min_size]
    vpaddd xmm4, xmm4, xmm1       ; clock 4 -- address + 5 + bitfields 1,2
    vmovd esi, xmm4               ; clock 5..6
    vmovd edx, xmm2               ; clock 5..6
    vmovdqu xmm6, [esi + edx]     ; clock 7..12
    

    然后再次重复相同的代码,仅使用xmm7代替xmm6。加载xmm6后,我们可能会处理xmm7

    此代码使用几个常量:

    min_size = 5, 0, 0, ...
    mask_12 = 0x0F, 0, 0, ...
    mask_34 = 0xF0, 0, 0, ...
    xmm5 = lookup table to add together two 2-bit values
    

    如此处所述实现的循环需要12个时钟来完成并且“跳跃”#39;一次有两个数据块。这意味着每个数据块有6个周期。这个数字可能过于乐观了。我不太确定MOVD只需要2个时钟。此外,还不清楚MOVDQU指令执行未对齐存储器负载的延迟是多少。我怀疑当数据跨越缓存行边界时,MOVDQU具有非常高的延迟。我想这意味着平均延迟时钟的另外一个时钟。因此,每个数据块大约7个周期是更现实的估计。


    使用蛮力

    每次迭代只跳一个或两个数据块很方便,但不能完全使用现代处理器的资源。在一些预处理之后,我们可以在下一个对齐的16字节数据中实现直接跳到第一个数据块。预处理应读取数据,计算每个字节的四个字段的总和,使用此总和来计算&#34;链接&#34;到下一个四字节字段,最后按照这些&#34;链接&#34;直到下一个对齐的16字节块。所有这些计算都是独立的,可以使用SSE / AVX指令集以任何顺序计算。 AVX2的预处理速度要快两倍。

    1. 使用MOVDQA加载16或32字节数据块。
    2. 将每个字节的4个位域相加。为此,使用两个PAND指令提取高和低4位半字节,使用PSRL *移位高半字节,使用两个PSHUFB找到每个半字节的总和,并使用PADDB添加两个和。 (6 uops)
    3. 使用PADDB计算&#34;链接&#34;到下一个四字段字节:将常量0x75,0x76,...添加到XMM / YMM寄存器的字节。 (1 uop)
    4. 关注&#34;链接&#34;使用PSHUFB和PMAXUB(PMAXUB的更昂贵的替代品是PCMPGTB和PBLENDVB的组合)。 VPSHUFB ymm1, ymm2, ymm2完成了几乎所有的工作。它取代了#34;越界&#34;值为零。然后VPMAXUB ymm2, ymm1, ymm2恢复原始&#34;链接&#34;代替这些零。两次迭代就足够了。在每个&#34;链接的每个迭代距离之后#34;是两倍大,所以我们只需要log(longest_chain_length)次迭代。例如,最长的链0-> 5-> 10-> 15-> X将在一步之后被压缩为0-> 10-> X并且在两步之后被压缩为0-> X. 。 (4 uops)
    5. 使用PSUBB从每个字节减去16,并且(仅限AVX2)使用VEXTRACTI128将高128位提取到单独的XMM寄存器。 (2 uops)
    6. 现在预处理已经完成。我们可以按照&#34;链接&#34;到下一个16字节数据块中的第一个数据块。这可以通过PCMPGTB,PSHUFB,PSUBB和PBLENDVB完成。但是,如果我们为可能的&#34;链接&#34;分配范围0x70 .. 0x80。值,单个PSHUFB将正常工作(实际上是一对PSHUFB,如果是AVX2)。值0x70 .. 0x7F从下一个16字节寄存器中选择适当的字节,而值0x80将跳过下一个16字节并加载字节0,这正是所需的。 (2次uop,延迟= 2个时钟)
    7. 这6个步骤的说明不需要按顺序排序。例如,步骤5和2的指令可以彼此相邻。每个步骤的指令应处理不同流水线阶段的16/32字节块,如下所示:步骤1处理块i,步骤2处理块i-1,步骤3,4处理块{{1等等。

      整个循环的延迟可能是2个时钟(每32个字节的数据)。但这里的限制因素是吞吐量,而不是延迟。当使用AVX2时,我们需要执行15次uops,这意味着5个时钟。如果数据不可压缩且数据块很大,则每个数据块大约需要3个时钟。如果数据是可压缩的并且数据块很小,则每个数据块大约有1个时钟。 (但由于MOVDQA延迟为6个时钟,要获得每32个字节5个时钟,我们需要两个重叠负载,并在每个循环中处理两倍的数据)。

      预处理步骤与步骤#6无关。所以它们可以在不同的线程中执行。这可能会减少5个时钟以下每32字节数据的时间。

答案 3 :(得分:3)

我不知道它可以采取多少个周期,而且我可能完全关闭,但是可以使用32位乘法与5个简单操作相加:

unsigned int sum = ((((byte * 0x10101) & 0xC30C3) * 0x41041) >> 18) & 0xF;

第一次乘法重复位模式

abcdefgh -> abcdefghabcdefghabcdefgh

第一位并且每6位保持一对:

abcdefghabcdefghabcdefgh -> 0000ef0000cd0000ab0000gh

第二次乘法对位模式求和(仅关注yyyy)

                     0000ef0000cd0000ab0000gh
             + 0000ef0000cd0000ab0000gh000000
       + 0000ef0000cd0000ab0000gh000000000000
 + 0000ef0000cd0000ab0000gh000000000000000000
 --------------------------------------------
   ..................00yyyy00................

最后两个操作将yyyy向右移动并切断左侧部分

主要问题是操作是顺序的......

修改

或者只是将整个事物向左翻译10位并删除最后一位:

unsigned int sum = (((byte * 0x4040400) & 0x30C30C00) * 0x41041) >> 28;

答案 4 :(得分:1)

这里有很多很棒的想法,但在讨论中很难找到它们。让我们用这个答案提供最终解决方案及其时间。请随时编辑这篇文章,并添加自己的时间。如果不确定底部代码中的时序粘贴,我会测量它。 x64组装最好。我很乐意编译C,但是在这个优化级别上很少有很好的结果而不需要很多调整。

<强>概述

将问题改为适当的上下文:目标是快速解码“Varint-GB”(或Group Varint)中已知的整数压缩格式。在其他地方,它在paper by Daniel Lemire and Leo Boytsov.中描述。我对本文第一版的标准“明显是作者是一个白痴”的风格做了评论,而丹尼尔(论文的主要作者,而不是一个白痴)狡猾地用我来帮助代码进行后续跟进。

标准变量(又名VByte)在每个字节的开头都有一个标志,确定它是否是整数的结尾,但解析起来很慢。此版本具有单字节“密钥”,然后是4个有效负载的压缩整数。密钥由4个2位字段组成,每个字段代表后面压缩整数的字节长度。每个可以是1字节(00),2字节(01),3字节(10)或4字节(11)。每个“块”长度为5到17个字节,但总是编码相同数量(4)的32位无符号整数。

Sample Chunk:
  Key:  01 01 10 00  
  Data: [A: two bytes] [B: two bytes] [C: three bytes] [D: one byte]
Decodes to: 00 00 AA AA   00 00 BB BB   00 CC CC CC  00 00 00 DD

密钥是16字节混洗模式表的索引,实际解码是通过使用PSHUFB将数据字节混洗到正确的间隔来完成的。

vec_t Data = *input
vec_t ShuffleKey = decodeTable[key]     
VEC_SHUFFLE(Data, ShuffleKey) // PSHUFB
*out = Data
实际上,通常通常还存在“增量解码”步骤,因为原始整数通常通过压缩整数之间的“delta”(差异)而不是整数本身而变得更小。但是解码例程的延迟通常无关紧要,因为下一次迭代不依赖于它。

问题重演

这里指出的问题是从一个'key'跳到下一个'key'。由于这里没有对解码数据的依赖(仅在键上),我将忽略实际的解码,只关注读取键的循环。该函数获取指向键和计数n的指针,并返回第n个键。

11个周期

“基本”方法是使用“预先”偏移的查找表,并将键作为索引。查找offsetTable中的任意256个键以获得预先计算的预算(sum + 1)偏移量。将其添加到当前输入位置,然后读取下一个键。根据英特尔的IACA,这个循环在Sandy Bridge上进行了11个循环(在Sandy Bridge上也是如此)。

uint32_t decodeBasic(uint8_t *in, size_t count) {
    uint64_t key, advance;
    for (size_t i = count; i > 0; i--) {
        key = *in;
        advance = offsetTable[key];
        in += advance;
    }
    return key;
}

0000000000000000 <decodeBasic>:
   0:   test   %rsi,%rsi
   3:   je     19 <decodeBasic+0x19>
   5:   nopl   (%rax)
   8:   movzbl (%rdi),%eax
   b:   add    0x0(,%rax,8),%rdi
  13:   sub    $0x1,%rsi
  17:   jne    8 <decodeBasic+0x8>
  19:   repz retq 

Block Throughput: 11.00 Cycles       Throughput Bottleneck: InterIteration
   0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
--------------------------------------------------------------
|           |     | 1.0   1.0 |           |     |     | CP | movzx eax, byte ptr [rdi]
| 0.3       | 0.3 |           | 1.0   1.0 |     | 0.3 | CP | add rdi, qword ptr [rax*8]
|           |     |           |           |     | 1.0 |    | sub rsi, 0x1
|           |     |           |           |     |     |    | jnz 0xffffffffffffffe7

10个周期

从那里开始,我们可以通过重新排列循环来减少10个周期,这样我们就可以添加更新输入指针并同时开始加载下一个键。您可能会注意到我必须使用内联汇编来“鼓励”编译器生成我想要的输出。我也会开始放弃外循环,因为它(通常)保持不变。

key = *in;
advance = offsetTable[key]; 
for (size_t i = count; i > 0; i--) {
    key = *(in + advance);
    ASM_LEA_ADD_BASE(in, advance);
    advance = offsetTable[key];
}

Block Throughput: 10.00 Cycles       Throughput Bottleneck: InterIteration
|  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
------------------------------------------------------------
|           |     | 1.0   1.0 |           |     |     | CP | movzx eax, byte ptr [rdi+rdx*1]
| 0.5       | 0.5 |           |           |     |     |    | lea rdi, ptr [rdi+rdx*1]
|           |     |           | 1.0   1.0 |     |     | CP | mov rdx, qword ptr [rax*8]
|           |     |           |           |     | 1.0 |    | sub rsi, 0x1
|           |     |           |           |     |     |    | jnz 0xffffffffffffffe2

9个周期

我之前尝试过使用POPCNT,但没有来自Ira和AShelly的建议,提示和想法,我没有太多运气。但是把这些碎片放在一起,我想我有一些东西在9个循环中运行循环。我把它放到实际的解码器中,Ints / s的数量似乎与此一致。这个循环基本上是在汇编中,因为我不能让编译器按照我想要的那样去做,多了多个编译器。

[编辑:AShelly删除了每条评论的额外MOV]

uint64_t key1 = *in;
uint64_t key2 = *in;
for (size_t i = count; i > 0; i--) {
    uint64_t advance1, advance2;
    ASM_POPCOUNT(advance1, key1);
    ASM_AND(key2, 0xAA);

    ASM_POPCOUNT(advance2, key2);
    in += advance1;

    ASM_MOVE_BYTE(key1, *(in + advance2 + 4));
    ASM_LOAD_BASE_OFFSET_INDEX_MUL(key2, in, 4, advance2, 1);        
    in += advance2;
 }


Block Throughput: 9.00 Cycles       Throughput Bottleneck: InterIteration
|  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
------------------------------------------------------------
|           | 1.0 |           |           |     |     | CP | popcnt r8, rax
| 1.0       |     |           |           |     |     | CP | and rdx, 0xaa
|           |     |           |           |     | 1.0 | CP | add r8, rdi
|           | 1.0 |           |           |     |     | CP | popcnt rcx, rdx
|           |     | 1.0   1.0 |           |     |     | CP | movzx rax, byte ptr [rcx+r8*1+0x4]
|           |     |           | 1.0   1.0 |     |     | CP | mov rdx, qword ptr [r8+rcx*1+0x4]
| 1.0       |     |           |           |     |     |    | lea rdi, ptr [rcx+r8*1]
|           |     |           |           |     | 1.0 |    | dec rsi
|           |     |           |           |     |     |    | jnz 0xffffffffffffffd0

作为现代处理器中运动部件复杂性的一个标志,我对这个例程的变化有了一个有趣的体验。如果我通过使用和(mov rax)指定内存位置,将第二个and rax, 0xaa行与mov rax, 0xAA; and rax, qword ptr [r8+rcx*1+0x4]结合起来,我最终得到了一个运行30%运行的例行程序。我认为这是因为有时导致循环的初始条件会导致负载的'和'微操作,并且在整个循环的POPCNT之前运行。

8个周期

任何?

<强>叶夫

这是我尝试实施Evgeny的解决方案。至少对于IACA的Sandy Bridge模型(到目前为止一直是准确的),我还没能把它降到9个周期。我认为问题在于,虽然ROR的延迟为1,但在P1或P5上需要两个微操作。要获得1的延迟,两者都必须可用。其他只是一个微操作,因此总是延迟为1. AND,ADD和MOV可以在P0,P1或P5上发出,但SHR不能在P1上。我可以通过添加一些额外的垃圾操作来接近10个周期,这会阻止ADD和AND取代SHR或ROR,但我不确定如何降到10以下。

Block Throughput: 10.55 Cycles       Throughput Bottleneck: InterIteration
|  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
------------------------------------------------------------
|           |     | 1.0   1.0 |           |     |     | CP | movzx eax, byte ptr [esi+0x5]
|           |     |           | 1.0   1.0 |     |     | CP | movzx ebx, byte ptr [esi+0x5]
| 0.2       | 0.6 |           |           |     | 0.3 |    | add esi, 0x5
| 0.3       | 0.3 |           |           |     | 0.3 |    | mov ecx, 0x3
| 0.2       | 0.2 |           |           |     | 0.6 |    | mov edx, 0x3
| 1.4       |     |           |           |     | 0.6 | CP | ror al, 0x4
| 0.1       | 0.7 |           |           |     | 0.2 | CP | and ecx, ebx
| 0.6       |     |           |           |     | 0.4 | CP | shr ebx, 0x6
| 0.1       | 0.7 |           |           |     | 0.2 | CP | add ebx, ecx
| 0.3       | 0.4 |           |           |     | 0.3 | CP | and edx, eax
| 0.6       |     |           |           |     | 0.3 | CP | shr eax, 0x6
| 0.1       | 0.7 |           |           |     | 0.2 | CP | add eax, edx
| 0.3       | 0.3 |           |           |     | 0.3 | CP | add esi, ebx
| 0.2       | 0.2 |           |           |     | 0.6 | CP | add esi, eax

答案 5 :(得分:1)

  mov al,1
  mov ah,2
  mov bl,3
  mov bh,4
  add ax,bx
  add al,ah