引言
最近发现项目代码中有一个对性能影响极大的for循环,该循环在处理2GB数据的时候,执行时间超长,导致程序直接卡死。因为最初没有涉及到大数据处理的场景,所以其对性能的影响是可以容忍的,但是最近开发了新的功能,需要处理大数据(GB),也用到了此处的代码,所以耗时的影响极其明显。
问题分析
先看如下示例代码:
class SampleData {
public:
void setData(char* data) {
int i = 0;
int word = _width / 32;
int n = word * 32;
for (; i < n; i += 32) {
process(_data, data, i, 32);
}
process(_data, data, i, _width - n);
}
private:
int _width;
char* _data;
};
问题就出在setData
这个方法中,其中_width
表示比特数,process
方法按照比特位处理数据,并填充到_data
中,经过分析发现,当process处理32位数据,且起始位置和32对齐时,等价于memcpy(_data, data, 4)
,因此上述循环完全可以避免,改成如下形式:
void setData(char* data) {
int word = _width / 32;
int n = word * 32;
memcpy(_data, data, word * 4);
process(_data, data, n, _width - n);
}
改完之后,再处理大数据时,不再出现卡死现象了。完美解决!
memcpy源码分析
秉着知其然知其所以然的态度,我又分析了下memcpy源码,他究竟是如何实现高性能字节数据的拷贝呢?首先看下其函数原型:
void *memcpy(void *dstpp, const void *srcpp, size_t len)
- dstpp表示复制的目的指针
- srcpp表示要被复制的源数据指针
- len表示复制的字节数
其源码如下:
#ifndef MEMCPY
#define MEMCPY memcpy
#endif
void *MEMCPY (void *dstpp, const void *srcpp, size_t len)
{
// 先将指针转换为无符号长整型
unsigned long int dstp = (long int) dstpp;
unsigned long int srcp = (long int) srcpp;
/* Copy from the beginning to the end. */
/* If there not too few bytes to copy, use word copy. */
// 如果有足够的数据(大于OP_T_THRES,其值一般为16或者8),就使用效率更高的拷贝(页拷贝、字拷贝)
if (len >= OP_T_THRES)
{
/* Copy just a few bytes to make DSTP aligned. */
// (-dstp) % OPSIZ是srcpp到最近的内存对齐地址的距离
len -= (-dstp) % OPSIZ;
// 前面没有对齐的内存使用字节拷贝
BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);
// 有可能使用页拷贝,此效率最高
PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);
// 不足一页的数据,使用字拷贝,如64位系统,一次拷贝8字节
WORD_COPY_FWD (dstp, srcp, len, len);
/* Fall out and copy the tail. */
}
/* There are just a few bytes to copy. Use byte memory operations. */
// 尾部剩余的不足一个字的数据,按照字节拷贝
BYTE_COPY_FWD (dstp, srcp, len);
return dstpp;
}
libc_hidden_builtin_def (MEMCPY)
从源码不难看出,其提升性能的方式主要在于:
- 进行内存对齐,内存对齐有利于提高cpu从内存取数据的效率。
- 一次拷贝足够多的数据,可以减少拷贝次数,从而也能提高性能。此处拷贝足够多的数据,主要依赖于指令集能够支持什么样的拷贝指令,如在x86架构中,常见的拷贝指令有movesb、movesw、movesd、movesq,而字拷贝就可以使用movesq指令。
- 相较于for循环,可以使用
rep movesq
指令,并配合相关的寄存器,就可以实现循环拷贝,比for循环使用了更少的指令,指令越少效率也就越高。
总结
- 在做内存数据拷贝的时候,应减少循环,尤其是嵌套循环,多使用memcpy,这样既容易理解,又可以提高程序性能,何乐而不为呢?
- 还有一种提升内存拷贝性能的方式,就是使用并行拷贝的方式。在编写程序的时候,可以考虑使用多个线程,每个线程拷贝其中一段内存。