memset

库函数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;
}
上一篇:ZBar源码分析——scanner.c(一) | 2021SC@SDUSC


下一篇:[P4551] 最长异或路径 题解