【发布时间】: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