前不久在腾讯面试的时候,被问到这点。面试官看我博客有一篇介绍标准库函数memcpy,然后问我怎么对标准库函数优化。当时现场没有啥思路,后来面试官让我从汇编角度想想,然后就有了思路。
标准库是逐个字节复制,代码如下。
void *memmove (void *dest, const void *src, size_t len)
{
char *d = dest;
const char *s = src;
if (d < s)
while (len--)
*d++ = *s++;
else
{
char *lasts = s + (len-1);
char *lastd = d + (len-1);
while (len--)
*lastd-- = *lasts--;
}
return dest;
}
标准库代码每次复制一个字节,效率确实有点低。CPU内部寄存器可能是8个字节长度。这表示我们每次最多可以复制8个,理论上提升8倍性能,具体要根据寄存器长度来看。当时给出的方案使用8字节复制,不足8字节则逐个字节复制。代码如下,考虑了地址重叠。
//优化版本
void *myMemmove2(void *dst, const void *src, size_t len)
{
//8字节长度
double *srcdb, *dstdb;
char *srcch, *dstch;
size_t times, left;
times = len / 8;
left = len % 8;
if (!src || !dst) return dst;
//从前往后复制
if (dst < src)
{
srcdb = (double *)src;
dstdb = (double *)dst;
while (times--)
{
*dstdb++ = *srcdb++;
len-=8;
}
srcch = (char *)srcdb;
dstch = (char *)dstdb;
while (left--)
{
*dstch++ = *srcch++;
}
}
//从后往前复制
else if (dst > src)
{
if (times > 0)
{
dstdb = (double *)(dst+len-8);
srcdb = (double *)(src+len-8);
}
while(times--)
{
*dstdb = *srcdb;
dstdb--;srcdb--;
}
if (len > 8 && left)
{
srcch = (char *)(srcdb+1)-1;
dstch = (char *)(dstdb+1)-1;
while(left--)
{
*dstch-- = *srcch--;
}
}
else
{
srcch = (char *)src + (len-1);
dstch = (char *)dst + (len-1);
while (len--)
*dstch-- = *srcch--;
}
}
return dst;
}
上述代码是优化的一种,其实还有优化的空间。比如,地址对齐问题。
如果地址不对齐,CPU可能需要取两次。因此,最好在进行复制之前进行字节对齐。有些方案就是先进行字节对齐再处理的。面试的时候,面试官提示了字节对齐问题,当时我的想法是,即便地址没对齐,CPU取两次值,然后进行裁剪拼接。也能够运行。不过,对于后面一点,即地址拼接,我不确定。字节对齐问题仅考虑了结构体所占大小这种。参考资料3里给出了内存对齐的相关资料,资料1给出了地址对齐的一种实现。
使用8字节对齐的方案,没有考虑地址对齐问题(编译器默认会分配对齐地址),进行了如下测试,分别使用未优化版本和优化版本对内存进行复制,两个函数各执行10000次,使用gprof进行性能统计,结果如下:
从上图可以看到,未优化版本每次调用耗费2.11ms,优化版本每次耗费0.28ms。因此,优化显然还是有效果。
参考资料:
1. 更快的memcpy (https://www.cnblogs.com/Leo_wl/p/3163057.html)
2. C/C++性能测试工具 GNU gprof (C/C++性能测试工具 GNU gprof - 书亚博园)
3. 内存对齐分配策略(含位域模式) (https://www.cnblogs.com/chaozhu/p/5600496.html)