13

我有四个 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相同的,并且都已设置为初始byte。奇怪的是,我想我可以免费做到这一点。因为我每个周期可以加载 2 次,而不是加载byte一次,我可以加载它四次:在第一个周期a和第二个周期。后两个负载将延迟一个周期,但直到第二个周期我才需要它们。下面的拆分表示事物应如何分解为单独的循环。dbc

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

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

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

编辑:

最初我说“8 个周期”,但正如 Evgeny 在下面指出的那样,我错了。正如 Evgeny 所指出的,唯一一次间接 4 周期加载是在不使用索引寄存器的情况下从系统内存的前 2K 加载。可以在英特尔架构优化手册第 2.12 节中找到正确的延迟列表

>    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

编辑:

我认为这就是 Ira 下面的解决方案将如何打破循环。我认为它还需要 5 个周期的工作后负载。

a = *byte
b = *byte

latency

latency 

latency

a &= 0x33
b >>= 2

b &= 0x33
c = a

a += b
c += b

a &= 7
c >>= 4

a += c 
4

6 回答 6

6

内置的 POPCOUNT 指令有帮助吗?

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

或者可能

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

不确定总时间。 这个讨论说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

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

于 2013-07-28T19:37:05.477 回答
5

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

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

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

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

64位寄存器查找

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

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

这里常量“5”意味着我们跳过当前字节的长度以及对应于全零长度的 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 指令快速提取位域

编辑:内森的测试揭示了以下方法的问题。Sandy Bridge 上的 ROR 指令使用两个端口,与 SHR 指令冲突。所以这段代码在 Sandy Bridge 上还需要 1 个时钟,这使得它不是很有用。可能它会在 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 个值。然后,您只需使用移位和掩码从该寄存器中获得正确的值。这个想法可以与任何计算总和的方法一起使用。这里它与简单的表查找一起使用:

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 个时钟:移位和掩码。这提供了 7 个时钟(但仅限于可压缩数据)。

如果将查表更改为计算,您可能会得到只有 6 个时钟的循环延迟:4 个用于将值相加并更新指针,2 个用于移位和掩码。有趣的是,在这种情况下,循环延迟仅由计算确定,而不取决于内存加载的延迟。


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

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

这个想法是在 AVX 中执行所有计算,除了应该从 GPR 完成的内存加载。(有一种替代方法可以在 AVX 中执行所有操作并在 Haswell 上使用广播+添加+收集,但它不太可能更快)。此外,将数据加载到一对 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而不是xmm6xmm6加载时我们可以处理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 个时钟来完成并一次“跳转”两个数据块。这意味着每个数据块有 6 个周期。这个数字可能过于乐观了。我不太确定 MOVD 只需要 2 个时钟。此外,还不清楚执行未对齐内存加载的 MOVDQU 指令的延迟是多少。我怀疑当数据跨越缓存线边界时 MOVDQU 具有非常高的延迟。我想这意味着平均增加 1 个延迟时钟。因此,每个数据块大约 7 个周期是更现实的估计。


使用蛮力

每次迭代只跳转一个或两个数据块很方便,但不会充分利用现代处理器的资源。经过一些预处理后,我们可以实现直接跳转到下一个对齐的 16 字节数据中的第一个数据块。预处理应该读取数据,计算每个字节的四个字段的总和,使用这个总和计算到下一个四字节字段的“链接”,最后沿着这些“链接”到下一个对齐的 16 字节块. 所有这些计算都是独立的,可以使用 SSE/AVX 指令集以任何顺序计算。AVX2 的预处理速度会快两倍。

  1. 使用 MOVDQA 加载 16 或 32 字节的数据块。
  2. 将每个字节的 4 个位域相加。为此,使用两条 PAND 指令提取高 4 位和低 4 位半字节,使用 PSRL* 移动高半字节,使用两个 PSHUFB 求每个半字节的总和,并使用 PADDB 将两个总和相加。(6 微秒)
  3. 使用 PADDB 计算到下一个四字段字节的“链接”:将常量 0x75、0x76、... 添加到 XMM/YMM 寄存器的字节。(1 微升)
  4. 按照 PSHUFB 和 PMAXUB 的“链接”(PMAXUB 更昂贵的替代品是 PCMPGTB 和 PBLENDVB 的组合)。VPSHUFB ymm1, ymm2, ymm2几乎所有的工作。它将“越界”值替换为零。然后VPMAXUB ymm2, ymm1, ymm2恢复原始“链接”来代替这些零。两次迭代就足够了。每次迭代后,每个“链接”的距离都是两倍大,所以我们只需要 log(longest_chain_length) 次迭代。例如,最长的链 0->5->10->15->X 一步压缩为 0->10->X,两步压缩为 0->X。(4 微指令)
  5. 使用 PSUBB 从每个字节中减去 16,并且(仅适用于 AVX2)使用 VEXTRACTI128 将高 128 位提取到单独的 XMM 寄存器中。(2 微秒)
  6. 现在预处理已经完成。我们可以跟随“链接”到下一个 16 字节数据中的第一个数据块。这可以通过 PCMPGTB、PSHUFB、PSUBB 和 PBLENDVB 完成。但是,如果我们0x70 .. 0x80为可能的“链接”值分配范围,则单个 PSHUFB 将正常工作(实际上是一对 PSHUFB,在 AVX2 的情况下)。值0x70 .. 0x7F从下一个 16 字节寄存器中选择正确的字节,而值0x80将跳过下一个 16 字节并加载字节0,这正是需要的。(2 微指令,延迟 = 2 个时钟)

这 6 个步骤的说明不需要按顺序排列。例如,第 5 步和第 2 步的说明可能彼此相邻。每个步骤的指令应处理流水线不同阶段的 16/32 字节块,如下所示:步骤 1 处理块i,步骤 2 处理块i-1,步骤 3,4 处理块i-2等。

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

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

于 2013-07-29T15:30:42.897 回答
4

Consider

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

Should be 9 machine instructions, many of them executed in parallel. (OP's first try is 9 instructions plus some moves not mentioned).

On inspection, this seems to have too many serial dependencies to be a win.

EDIT: The discussion about binary-ops being destructive, and LEA avoiding this, got me to thinking about how to use LEA to combine more than one operand, and multiplication by constants. The above code tries to right normalize the answer by shifting right, but we can left-normalize the answer by multiplying. With that insight, this code might work:

     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

Well, entertaining but still didn't pan out. 3 clocks isn't very long :-{

于 2013-07-26T12:02:51.170 回答
3

我不知道它可能需要多少个周期,我可能会完全关闭,但可以使用 32 位乘法对 5 个简单的操作求和:

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

第一次乘法重复位模式

abcdefgh -> abcdefghabcdefghabcdefgh

第一位和每 6 位保持一对:

abcdefghabcdefghabcdefgh -> 0000ef0000cd0000ab0000gh

第二次乘法对位模式求和(只有 yyyy 感兴趣)

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

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

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

编辑

或者只是将整个内容向左平移 10 位并删除最后一位:

unsigned int sum = (((byte * 0x4040400) & 0x30C30C00) * 0x41041) >> 28;
于 2013-07-30T21:50:53.450 回答
2

这里有很多很棒的想法,但是在讨论中很难找到它们。让我们使用这个答案来提供最终解决方案及其时间安排。请随时编辑此帖子并添加您自己的帖子以及时间。如果不确定底部代码中的时间粘贴,我会测量它。x64 程序集最好。我会很高兴地编译 C,但在没有大量调整的情况下,在这种优化级别上很少有好的结果。

概述

重新表述问题以将其置于适当的上下文中:目标是快速解码以“Varint-GB”(或 Group Varint)已知的整数压缩格式。在其他地方,它在Daniel Lemire 和 Leo Boytsov 的论文中有所描述。. 我对这篇论文的第一版标准“显然作者是个白痴”风格发表了评论,Daniel(论文的主要作者,而不是一个白痴)狡猾地拉拢我来帮助编写后续代码.

标准 Varint(又名 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

实际上,通常也有一个“增量解码”步骤,因为通常通过压缩整数之间的“增量”(差异)而不是整数本身来使原始整数变小。但是解码例程的延迟通常并不重要,因为下一次迭代不依赖于它。

问题重述

这里指定的问题是从一个“键”跳到下一个。由于这里对解码的数据没有依赖关系(仅对密钥),我将忽略实际的解码,只专注于读取密钥的循环。该函数接受一个指向键的指针和一个计数 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

作为现代处理器中移动部件的复杂性的一个标志,我对这个例程的一个变体有一个有趣的体验。如果我通过用 and ( )指定内存位置将第二mov rax行与 我认为这是因为有时导致循环的初始条件会导致加载/和的“和”微操作在整个循环的 POPCNT 之前运行。and rax, 0xaamov rax, 0xAA; and rax, qword ptr [r8+rcx*1+0x4]

8个周期

任何人?

叶夫根尼

这是我尝试实施 Evgeny 的解决方案。我还不能把它降到 9 个周期,至少对于 IACA 的 Sandy Bridge 模型(到目前为止它是准确的)。我认为问题在于,虽然 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
于 2013-07-30T02:32:57.577 回答
0
  mov al,1
  mov ah,2
  mov bl,3
  mov bh,4
  add ax,bx
  add al,ah
于 2013-07-30T08:01:10.013 回答