【问题标题】:Why for accessing elements of char array byte transffer is used为什么访问char数组的元素使用字节传输
【发布时间】:2018-03-13 00:01:36
【问题描述】:

让我们考虑这个非常简单的代码

int main(void)
{
    char buff[500];
    int i;
    for (i=0; i<500; i++)
    {
        (buff[i])++;
    }   
}

所以,它只经过 500 个字节并增加它。此代码在 x86-64 架构上使用 gcc 编译,并使用 objdump -D 实用程序进行反汇编。查看反汇编代码,我发现数据是从内存逐字节传输到寄存器的(参见,movzbl指令用于从内存中获取数据,mov %dl用于将数据存储到内存中)

00000000004004ed <main>:
  4004ed:       55                      push   %rbp
  4004ee:       48 89 e5                mov    %rsp,%rbp
  4004f1:       48 81 ec 88 01 00 00    sub    $0x188,%rsp
  4004f8:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  4004ff:       eb 20                   jmp    400521 <main+0x34>
  400501:       8b 45 fc                mov    -0x4(%rbp),%eax
  400504:       48 98                   cltq   
  400506:       0f b6 84 05 00 fe ff    movzbl -0x200(%rbp,%rax,1),%eax
  40050d:       ff 
  40050e:       8d 50 01                lea    0x1(%rax),%edx
  400511:       8b 45 fc                mov    -0x4(%rbp),%eax
  400514:       48 98                   cltq   
  400516:       88 94 05 00 fe ff ff    mov    %dl,-0x200(%rbp,%rax,1)
  40051d:       83 45 fc 01             addl   $0x1,-0x4(%rbp)
  400521:       81 7d fc f3 01 00 00    cmpl   $0x1f3,-0x4(%rbp)
  400528:       7e d7                   jle    400501 <main+0x14>
  40052a:       c9                      leaveq 
  40052b:       c3                      retq   
  40052c:       0f 1f 40 00             nopl   0x0(%rax)

看起来它对性能有一些影响,因为在这种情况下,您必须访问内存 500 次才能读取和 500 次才能存储。我知道缓存系统会以某种方式应对它,但无论如何。 我的问题是为什么我们不能加载四字,只做几个位操作来增加该四字的每个字节,然后将其推回内存?显然,它需要一些额外的逻辑来处理小于四字的数据的最后部分和一些额外的寄存器。但是这种方法将大大减少内存访问次数,这是最昂贵的操作。可能我没有看到一些阻碍这种优化的障碍。所以,在这里得到一些解释会很棒。

【问题讨论】:

  • 缓存处理加载和存储,可能以 64 字节块为单位。
  • 是的,我知道在第一次读取整个缓存行后将完成,其余的访问将由缓存子系统解决。那么,您的意思是说处理四字字节所需的额外操作比访问缓存的成本更高?
  • 如果您将 8 个字节加载到 rdx 中,则没有简单的方法可以单独递增字节而不冒溢出到下一个字节的风险。无论如何,真正读取和写入 RAM 所需的时间会让 CPU 在“等待”时执行数百条指令。
  • 您可能想看看优化后生成的内容:godbolt.org/g/QWvV7S。您可以看到编译器使用 SIMD 指令来执行操作。如果将数组的长度从 500 更改为更小的数字,您可以看到代码如何根据数组长度发生变化。代码是以这种方式完成的,以允许在不优化整个事情的情况下生成代码
  • 但是这种方法会显着减少内存访问次数,这是最昂贵的操作:不,触摸你之前已经触摸过几条指令的缓存线很便宜.它在 L1d 缓存中仍然很热。代价高昂的是缓存未命中或分散的内存访问模式。你从-O0 得到的汇编显然是可怕的,并且每 6 个周期增加一个瓶颈,因为它将 循环计数器 保存在内存中。见stackoverflow.com/questions/49189685/…

标签: c assembly memory-management


【解决方案1】:

不应该这样做的原因:想象一下,如果 char 碰巧是无符号的(使溢出具有定义的行为)并且您有一个字节 0xFF 后面(或前面,取决于字节序)0x1 .

一次增加一个字节,您最终会得到0xFF 变成0x000x01 变成0x02。但是,如果您一次只加载 4 或 8 个字节并添加 0x01010101(或等效的 8 个字节)以达到相同的结果,0xFF 将溢出到0x01,因此您最终会得到0x000x03,而不是 0x000x02

签名的char 通常也会出现类似的问题;有符号溢出和截断规则(或缺少规则)使其更加复杂,但要点是一次增加一个字节会限制对该字节的影响,而不会出现跨字节“干扰”。

【讨论】:

  • 感谢您的解释!但最初我认为不仅仅是将 0x01010101 添加到加载的双字中。假设在 eax 中有 4 个字节需要修改。我可以放入像 0x000000FF 这样的 ebx 掩码,它可以移动到 8 位以访问 eax 中的每个特定字节。像 edx = eax & ebx; ebx
  • 我会注意到,在 x86 芯片上,PADDB in 可能实际上允许这种没有进位行为的环绕;据我所知,一些编译器可能会选择使用它,但它不太可能获得太多收益,因为它可能最终会受到内存限制。
  • @AndrewBolotov:给定 CPU 缓存,用单个四字移动和一堆额外的位操作替换几个字节移动指令不太可能获得什么。
  • @ShadowRanger:即使数据在缓存中一开始是冷的,单独的字节增量也不会受内存限制。在现代桌面上,每个核心时钟周期加载 8 个字节对于顺序访问来说并非不合理。 (并且每个时钟读取/修改/写入 4 个字节也不是不合理的。)对于适合 L1d 缓存的像这样的小数组(只有 500 个字节),paddb 应该提供将近 16 倍的加速。并且启用优化而不是braindead gcc -O0 代码进行编译将提供约6 个加速的另一个因素。循环计数器的存储/重新加载是瓶颈!
  • 对于没有 SIMD 字节添加的目标架构,一个好的编译器可能能够使用SWAR (SIMD within a register。也许加载一个(32 位)字并用0xFF00FF00 和反相屏蔽它,将0x01010101 添加到两者,再次屏蔽以将进位输出归零,然后将它们重新组合在一起。 (也许更好的算法是可能的,但是从每个字节生成进位是很困难的)。这可能只是 64 位字的胜利,或者如果 ALU 吞吐量与存储吞吐量相比非常好。 (比现代 x86 更不平衡,每个周期约 4 个 ALU 操作与每个周期 1 个存储)。
【解决方案2】:

当您在没有优化的情况下进行编译时,编译器会将代码更直接地翻译为汇编,部分原因是当您在调试器中单步执行代码时,这些步骤与您的代码相对应。

如果启用优化,则程序集可能看起来完全不同。

此外,您的程序通过读取未初始化的char 会导致未定义的行为。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-09-26
    • 2018-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-09
    • 2018-02-05
    相关资源
    最近更新 更多