我需要分析一个执行大量数组副本的应用程序,所以我最终分析了这个非常简单的函数:
typedef unsigned char UChar;
void copy_mem(UChar *src, UChar *dst, unsigned int len) {
UChar *end = src + len;
while (src < end)
*dst++ = *src++;
}
我使用 Intel VTune 进行实际分析,从那里我发现使用 gcc -O3 和“普通”gcc (4.4) 进行编译时存在显着差异。
为了了解原因和方式,我获得了两次编译的汇编输出。
未优化的版本是这个:
.L3:
movl 8(%ebp), %eax
movzbl (%eax), %edx
movl 12(%ebp), %eax
movb %dl, (%eax)
addl $1, 12(%ebp)
addl $1, 8(%ebp)
.L2:
movl 8(%ebp), %eax
cmpl -4(%ebp), %eax
jb .L3
leave
所以我看到它首先从 *src 加载一个双字并将低字节放入 edx,然后将其存储到 *dst 并更新指针:足够简单。
然后看到优化版,啥也不懂。
编辑:这里有优化的组件。
因此我的问题是:gcc 可以在这个函数中进行什么样的优化?
优化后的代码相当混乱,但我可以发现 3 个循环(靠近 L6、L13 和 L12)。我认为 gcc 做了@GJ 的建议(我给他投了票)。 L6 附近的循环每次移动 4 个字节,而循环 #2 只移动 1 个字节,并且有时只在循环 #1 之后执行。我仍然无法获得循环#3,因为它与循环#2 相同。
未优化的函数逐字节移动字节!
如果您先计算长度,那么您可以一次移动 4 个字节,其余 1..3 个字节手动移动。如果您可以确保正确的(4 字节)内存对齐,则复制功能也应该更快。 并且不需要递增堆栈上的指针,可以使用寄存器。 所有这些都将极大地提高功能速度。
或者使用专用的 mem move 函数,如 memmove!
优化的类型取决于函数及其属性,如果函数被标记为内联,并且足够小,它将变成
MOV
的展开循环,这比基于 REP
的变体更快(并且可以避免寄存器溢出)。对于未知大小,您将获得 REP MOVS
系列指令(从最大字大小开始,以减少恒定大小的循环量,否则它将使用您复制的数据单元的大小)。
如果启用了 SSE,它很可能会在长度允许的情况下使用展开的未对齐移动 (
MOVDQU
) 或循环未对齐移动(不知道是否会使用时间预取,从中获得的收益取决于块大小),如果长度足够大。如果源/目标正确对齐,它将尝试使用更快对齐的变体。
就目前而言,当它未内联时,您可以使用该功能的最佳方式是
MOVSB
。
gcc 可以生成的最快的 x86 汇编指令是
rep movsd
,它一次复制 4 个字节。 memcpy
中的标准 libc 函数 <string.h>
与 memcpy
中的特殊内联 gcc 以及 <string.h>
中的许多其他函数一起为您提供最快的结果。
您还可以从此处使用 restrict 中受益。