库函数memset的更有效版本实现
有参考其他的
一、实验预备知识
(1)库函数 memset
简介:memset
函数作用:将从 s 开始的 n 个字节的内存区域全都填充为 c 的低位字节。它是对较大的结构体或数组进行清零操作的一种最快方法。
(2)库函数memset
的最直接实现:
/* Basic implementation of meset */
void *basic_memset(void *s, int c, size_t n)
{
size_t cnt = 0;
unsigned char *schar = (unsigned char *)s;
while (cnt < n)
{
*schar++ = (unsigned char) c;
cnt++;
}
return s;
}
(3)CPE:cycles per element,每元素的周期数,是度量程序性能的指标。CPE越低,程序运行速度越快。因此,优化程序的一个目标是减少CPE。
(4)size_t:它是一个与机器相关的 unsigned 类型,定义在 cstddef 头文件中,其大小足以保证存储内存中对象的大小。设计 size_t 是为了适应多个平台,增强程序在不同平台上的可移植性。通常使用 sizeof() 操作得到的结果就是 size_t 类型的。在64位系统中为 long long unsigned int ,大小为8个字节。
二、实验目的
(1)实验预期:实现 memset
函数的更有效的版本,在参考机上,能做到把CPE从直接实现的1.00降低到0.127,也就是程序在每个周期可以写8个字节。
(2)实验可行性分析:
从库函数 memset
直接实现方式可以看出,填充的区域长度 n 是 size_t 类型的,用于填充的数据 c 是 int 类型的,且实现时我们将 c 强制转换为 unsigned char 类型。在64位系统里,int类型为4个字节,size_t 是8个字节,而 unsigned char 是1个字节的,也就是我们用c的最低位字节来填充长度为n的区域。
联系所学的代码优化方法,基本的代码移动、共用表达式(已有的表达式重复使用不额外重复计算)等方法在此处不能很好地改善代码效率。
观察发现,每次我们只填充1个字节的长度,需要填充 n 次。那么,可以考虑利用循环展开的方式,每次填充 k 个字节,这样在 n%k = 0 的情况下只需要填充 n/k 次即可,当 n 不为 k 的倍数时另做考虑。但很容易知道,这种方法可以减少循环次数,最终可以降低此程序的CPE,提高程序运行速度。
三、实验设计
根据(二)中的实验可行性分析,我们考虑采用循环展开方式进行程序优化。
在这里,我尝试在填充时每次使用数据类型为unsigned long的字来进行填充。unsigned long类型标准为4字节或8字节,在本机系统上,其为4字节的。
记 K = sizeof(unsigned long),我们参考原有的 memset
实现来进行改进。
- 当n是K的倍数时,可将unsigned char类型的s强制转换为unsigned long类型,即为unsigned long* slong。基于此进行 n/K 次循环,每次循环填充K个字节大小的区域。每填充一次slong自增一次,指向下一个需要填充的起始位置。
- 当n不是K的倍数时,可以考虑在最后几次迭代依旧利用 unsigned char 类型的数据来填充进行解决。我们记 main_body = n/K, rest = n%K,那么可以进行 main_body 次循环,这 main_body 次循环里用数据类型为 unsigned long 的数据填充,剩余的 rest 次用数据类型为 unsigned char 的类型来填充。同样的,每次填充指针需要自增一次,指向下一个需要填充的起始位置。
- 最后考虑关于对齐的问题。根据计算机组成原理的知识,我们知道,针对不同字长的数据在主存中的存储,有边界对齐和边界不对齐两种存储方式。在某些机器上,未对齐的写操作可能比对齐的写操作慢很多。因此,我们需要做到,从开始位置一直到目的地址是 K 的倍数时,使用字节级的方式进行写操作(也就是用 unsigned char 类型数据填充),假设写了 m 个字节。这样以后,考虑剩下的区域长度 n’=n-m,先判断 n’%K 的值是否为0,再分别采用上面提到的两种方式来进行填充。
四、实验结果
采用这种设计方法进行memset
库函数的优化,可以改进代码的性能。利用 clock() 函数可以计算代码运行所消耗的时钟周期数。通过与memset
的最直接实现进行比较,我们会发现利用循环展开方式进行优化后代码性能有所提升。
五、源代码
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// memset库函数的更有效实现,填充数据改为 unsigned long 类型
void* eff_memset(void* s, unsigned long c, size_t n)
{
size_t K = sizeof(unsigned long); //本机上K=4
size_t cnt = 0; //记循环次数
unsigned char* schar = (unsigned char*)s; //将s强制转换为unsigned char类型
while (cnt < n)
{
if ((size_t)schar % K == 0) //判断开始位置是否为K的倍数
break;
//开始地址不是4的倍数,先用unsigned char类型进行填充
*schar++ = (unsigned char)c;
cnt++;
}
unsigned long* slong = (unsigned long*)schar; //将s强制转换为unsigned long类型
size_t num = n - cnt; //去除已填充的区域
size_t main_body = num / K;
size_t rest = num % K;
//主体采用循环展开方式用unsigned long类型数据填充
for (size_t i = 0; i < main_body; i++)
*slong++ = c;
//针对num不是K的倍数时,收尾部分用unsigned char类型数据填充
schar = (unsigned char*)slong;
for (size_t i = 0; i < rest; i++)
*schar++ = (unsigned char)c;
return s;
}
int main(int argc, char* argv[])
{
size_t space = sizeof(char) * 256;
void* eff_space = malloc(space);
unsigned long fill = ~0;
eff_memset(eff_space, fill, space);
free(eff_space);
return 0;
}