百万点数组下memset、memcpy与for循环效率对比及原理分析
一.概述
做上百万数组赋值及拷贝的时候,不得不考虑效率问题,一般计算机低于十万的数组赋值拷贝基本不用考虑效率问题(基本在1毫秒内完成),本文会对百万以上的数组下赋值及拷贝进行效率分析和对比。
二.代码实测
1.测试环境:QT5
2.测试代码
#include <QTime>
#include <QDebug>
#include <stdio.h>
#include <string.h>
#define ARRAY_SIZE 5000000 //五百万
//必须定义为全局数组,不能定义为局部变量,数组太大,定义为局部变量运行会崩溃
double array1[ARRAY_SIZE];
// 定义第二个包含100万个double元素的数组
double array2[ARRAY_SIZE];
// 用于赋值的源数组
double source_array[ARRAY_SIZE];
int main(int argc, char *argv[])
{
// 定义第一个包含100万个double元素的数组
for (int i = 0; i < ARRAY_SIZE; i++) {
source_array[i] = (double)i;
}
qDebug() << "当前时间1:" << QTime::currentTime().toString("hh:mm:ss.zzz");
// 使用for循环将array1清零
for (int i = 0; i < ARRAY_SIZE; i++) {
array1[i] = 0.0;
}
qDebug() << "当前时间11:" << QTime::currentTime().toString("hh:mm:ss.zzz");
// 使用memset将array1再次清零
qDebug() << "当前时间2:" << QTime::currentTime().toString("hh:mm:ss.zzz");
memset(array1, 0, sizeof(double) * ARRAY_SIZE);
qDebug() << "当前时间22:" << QTime::currentTime().toString("hh:mm:ss.zzz");
// 使用for循环对array2进行赋值
qDebug() << "当前时间3:" << QTime::currentTime().toString("hh:mm:ss.zzz");
for (int i = 0; i < ARRAY_SIZE; i++) {
array2[i] = source_array[i];
}
qDebug() << "当前时间33:" << QTime::currentTime().toString("hh:mm:ss.zzz");
// 使用memcpy对array2进行内存拷贝赋值
qDebug() << "当前时间4:" << QTime::currentTime().toString("hh:mm:ss.zzz");
memcpy(array2, source_array, sizeof(double) * ARRAY_SIZE);
qDebug() << "当前时间44:" << QTime::currentTime().toString("hh:mm:ss.zzz");
return 0;
}
3.测试结果
五百万点的情况下log打印:
当前时间1: "10:27:43.122"
当前时间11: "10:27:43.136"
当前时间2: "10:27:43.136"
当前时间22: "10:27:43.143"
当前时间3: "10:27:43.143"
当前时间33: "10:27:43.158"
当前时间4: "10:27:43.158"
当前时间44: "10:27:43.163"
可以看到memset和memcpy是for循环时间的40%~50%,也就是效率能提升一倍。
三.原理分析
1.函数实现的底层优化
memset 和 memcpy 的底层实现:memset 和 memcpy 是标准库函数,它们在不同的操作系统和编译器中都经过了高度的底层优化。这些函数通常是用汇编语言实现的,汇编语言可以直接操作计算机的底层硬件,绕过了高级语言的一些开销。
示例:在 x86 架构上,memcpy 可能会使用 SSE(Streaming SIMD Extensions)或 AVX(Advanced Vector Extensions)等指令集。这些指令集允许在一个时钟周期内同时处理多个数据元素,从而显著提高了数据复制的速度。
2.减少函数调用开销
for 循环的函数调用开销:当使用 for 循环来复制或设置内存时,每次循环都可能涉及到一些额外的操作,如循环条件的判断、循环变量的更新等。这些操作在每次循环时都会重复执行,增加了程序的开销。
memset 和 memcpy 的一次性调用:memset 和 memcpy 是一次性调用的函数,它们在函数内部完成所有的内存操作,避免了多次函数调用的开销。
3.数据访问模式的优化
for 循环的数据访问模式:在 for 循环中,每次循环通常只处理一个数据元素,这种逐元素的访问模式可能会导致频繁的内存访问,从而降低了效率。
memset 和 memcpy 的批量数据处理:memset 和 memcpy 会以更大的块为单位来处理数据,这样可以减少内存访问的次数,提高缓存命中率。例如,memcpy 可能会一次复制 4 字节、8 字节甚至 16 字节的数据,而不是每次只复制一个字节。
4.编译器优化
for 循环的编译器优化限制:虽然现代编译器会对 for 循环进行一些优化,但由于 for 循环的逻辑可能比较复杂,编译器的优化能力有限。
memset 和 memcpy 的编译器优化:编译器通常会对 memset 和 memcpy 进行特殊的优化,因为它们是标准库函数,编译器可以更好地预测它们的行为,并进行针对性的优化。