【C语言】cache和程序访问的局部性对程序性能的影响

2023-12-17 19:52

本文主要是介绍【C语言】cache和程序访问的局部性对程序性能的影响,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1.源程序比较其性能影响
  • 2.内存分配
    • (1)静态存储区(static):
    • (2)栈区(stack):
    • (3)堆区(heap):
    • (4)文字常量区:
    • (5)程序代码区:
    • 举例:
    • 注意字符串“abc”与“123”的区别:
  • 3.程序执行时间
    • (1)`time( )`
    • (2)`clock( )`
    • (3)`QueryPerformanceCounter( )`
  • (4)`GetTickCount( )`
  • (5)`RDTSC指令`
    • 上述计时方法的比较:
  • 源程序和执行结果分析:
    • 源程序和执行结果
      • 1. 给出源程序(文本文件)和执行结果。
      • 2.对实验结果进行分析,说明局部数据块大小、数组访问顺序等和执行时间之间的关系。
      • (3)分析说明数组a分配在静态存储区、堆区和栈区,对for循环段的执行效率有没有影响。
  • 总结

通过实际程序的执行结果,了解程序访问的局部性对带有cache的计算机系统性能的影响。

1.源程序比较其性能影响

在以下程序中,修改或添加必要的语句(如添加计时函数等),以计算和打印主体程序段(即for循环段)的执行时间。分别以M=10、N=100000;M=1000、N=1000;M=100000、N=10;执行程序A和程序B,以比较两种for循环段执行时间的长短。
下列程序中给出的数组a是局部临时变量,分配在栈中,也可改用静态全局变量,或在堆中动态申请空间。

  • 程序段A:
void assign_array_rows( )
{int i, j;short a[M][N];for(i= 0; i<M; i++)for(j= 0; j<N; j++)a[i][j]=0;
}
  • 程序段B:
void assign_array_cols( )
{int i, j;short a[M][N];for(j= 0; j<N; j++)for(i=0; i<M; i++)a[i][j]=0;
}

2.内存分配

可编程内存基本上分为:静态存储区、堆区和栈区等。它们的功能不同,使用方式也不同。

(1)静态存储区(static):

在程序编译的时候就已经分配好,这块内存在程序的运行期间都存在,程序结束后由系统释放。它主要存放静态数据、全局数据和常量。

(2)栈区(stack):

由编译器自动分配、释放,无需手工控制。在执行函数时,用来存放函数内局部变量,函数执行结束时这些存储单元自动被释放。
栈区是向低地址扩展的数据结构,是一块连续的内存的区域。栈区的操作方式类似于数据结构中的栈,是一个先进后出的队列,进出一一对应,不会产生碎片。
栈内存分配运算内置于处理器的指令集中,效率很高。栈区栈顶的地址和栈的最大容量是系统预先规定好的,在 Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),因此,栈区分配的内存容量较小。如果申请的空间小于栈的剩余空间,系统会为程序分配内存,如果申请的空间超过栈的剩余空间,系统会报异常,提示栈溢出overflow

(3)堆区(heap):

由程序员分配、释放,若程序员不释放,程序结束时可能由OS回收。亦称动态内存分配,程序在运行的时候用mallocnew申请任意大小的内存,程序员自己负责在适当的时候用freedelete释放内存,容易产生memory leak(内存泄漏)
堆区操作由C/C++函数库提供,机制很复杂,所以堆的效率比栈低很多;另外,频繁的new/delete造成大量碎片,也会使程序效率降低。
堆区分配方式类似于链表。系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删除空闲结点链表中的该结点,并将该结点空间分配给程序。大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间;另外,系统会将多余的部分重新放入空闲链表中。堆区大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G),所以堆的空间比较灵活,比较大。
堆是向高地址扩展的数据结构,是不连续的内存区域(因为系统是用链表来存储堆区的空闲内存地址的,自然堆区是不连续的内存区域,而链表的遍历方向是由低地址向高地址)。

(4)文字常量区:

存放常量字符串,程序结束后由系统释放。

(5)程序代码区:

存放程序的二进制代码。

举例:

#include <stdio.h>
#include <stdlib.h>
int a=0;//全局变量,存放在静态存储区
int main()
{ int b;//局部变量,存放在栈区static int c;//静态局部变量,存放在静态存储区char s[]="abc";//数组的元素属于局部变量,存放在栈区char *p="123";//字符串常量"123"存放在静态存储区,指针变量p存放在栈区char *p1=(char *)malloc(10);//指针变量p1存放在栈区,申请来得10个字节的区域在堆区
}

注意字符串“abc”与“123”的区别:

  1. 字符串“abc”、“123”存放在不同的区域,因为“abc”存在于数组中,即存储于栈区,而“123”为字符串常量,存储在静态存储区。
  2. “abc”是在程序运行时赋值的,而“123”是在编译时就确定的。
  3. 假设有如下语句
    s[0]='0';
    p[0]='0';
    语句①没有问题,“abc”存放于数组中,此数据存储于栈区,对它修改是没有任何问题的。
    但语句②有问题,“123”为字符串常量,存储在静态存储区,虽然通过p[0]可以访问到静态存储区中的第一个数据,即字符‘l’所在的存储的单元。但是因为“123”为字符串常量,不可以改变,所以语句“p[0]='0';”在程序运行时,会报告内存错误。

3.程序执行时间

通常先调用计时函数记下当前时间tstart,然后处理一段程序,再调用计时函数记下处理后的时间tend,就可以得到程序的执行时间(t = tend - tstart)
常用计时方法有:time、clcok、QueryPerformanceCounter、GetTickCount、RDTSC

(1)time( )

获取当前的系统时间,返回的结果是一个time_t类型,其实就是一个大整数,其值表示从CUT(Coordinated Universal Time)时间1970年1月1日00:00:00(称为UNIX系统的Epoch时间)到当前时刻的秒数。

#include <stdio.h>
void fun( )
{//dosomethinglong i;for(i=0;i<1000000000;i++);}
int main()
{ time_t start,stop;//定义时间类型变量start = time(NULL);fun(); stop = time(NULL);printf("Use Time:%ld\n",(stop-start));
}
  • 程序运行结果:Use Time:1

(2)clock( )

函数返回从“开启这个程序进程”到“程序中调用clock( )函数”时之间的CPU时钟计时单元(clock tick)数,在MSDN中称之为挂钟时间(wal-clock)。常量CLOCKS_PER_SEC表示一秒钟会有多少个时钟计时单元。

#include <stdio.h>
#include <time.h>//包含头文件
void fun( )
{//dosomethinglong i;for(i=0;i<1000000000;i++);}
int main()
{ double dur;clock_t start,end;//定义时钟类型变量start = clock();fun(); end = clock();dur = (double)(end - start);printf("Use Time:%f\n",(dur/CLOCKS_PER_SEC));//时钟个数除以时钟频率即为所花时间
}
  • 程序运行结果:Use Time:1.980000

(3)QueryPerformanceCounter( )

函数返回高精确度性能计数器的值,它可以以微妙为单位计时。但是QueryPerformanceCounter()确切的精确计时的最小单位是与系统有关的,所以,必须要查询系统以得到QueryPerformanceCounter()返回的嘀哒声的频率。QueryPerformanceFrequency()提供了这个频率值,返回每秒嘀哒声的个数。

#include <stdio.h>  
#include <windows.h>//包含头文件 
void fun( )
{//dosomethinglong i;for(i=0;i<1000000000;i++);    
}
int main()  
{    LARGE_INTEGER t1,t2,tc;QueryPerformanceFrequency(&tc);QueryPerformanceCounter(&t1);fun();QueryPerformanceCounter(&t2);printf("Use Time:%f\n",(t2.QuadPart - t1.QuadPart)*1.0/tc.QuadPart);//前后滴答数之差除以1秒钟的滴答数,即得时间秒数
}  
  • 程序运行结果:Use Time:1.821330

(4)GetTickCount( )

函数返回从操作系统启动到现在所经过的毫秒数,它的返回值是DWORD

#include <stdio.h>  
#include <windows.h>//包含头文件 
void fun( )
{//dosomethinglong i;for(i=0;i<1000000000;i++);    
}
int main()  
{    DWORD t1,t2;t1 = GetTickCount();fun();//dosomethingt2 = GetTickCount();printf("Use Time:%f\n",(t2-t1)*1.0/1000);//将毫秒值转成秒值
}  
  • 程序运行结果:Use Time:1.937000

(5)RDTSC指令

rdtsc 指令是 x86 架构下的一条汇编指令,全称 “Read Time-Stamp Counter”,用于读取时间戳计数器(TSC),CPU 的每个时钟周期会对其(TSC)进行加一操作。通过rdtsc指令读取这个计数器,可以测量程序某段代码的执行时间。
时间戳计数器(TSC)是一个64位的寄存器,低32位存储在EAX寄存器(在 64 位架构下为RAX寄存器),高32位存储在EDX寄存器(在 64 位架构下为RDX寄存器)。通常使用内联汇编“__asm__ volatile("rdtsc" : "=a"(low), "=d"(high));”将高32位和低32位组合成一个64位无符号整数,作为rdtsc( )函数的返回值。

#include <stdio.h>  
#include <stdint.h>//包含头文件
#define FREQUENCY (3.2*1e9)
//3.2GHz,需要事先查看自己电脑的CPU工作频率,定义成符号常量
void fun( )
{//dosomethinglong i;for(i=0;i<1000000000;i++);    
}
static inline uint64_t rdtsc()
{uint32_t low, high;__asm__ volatile("rdtsc" : "=a"(low), "=d"(high));return (uint64_t)high << 32 | low;
}
int main()  
{    uint64_t t1,t2;t1 = rdtsc();fun();t2 = rdtsc();printf("Use Time:%f\n",(t2 - t1)*1.0/FREQUENCY);//FREQUENCY为CPU的频率
}
  • 程序运行结果:Use Time:1.788094

上述计时方法的比较:

源程序和执行结果分析:

源程序和执行结果

1. 给出源程序(文本文件)和执行结果。

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>#define M 10  
#define N 100000  // 静态存储区数组
static short staticArray[M][N];void assign_array_rows_static() {int i, j;for (i = 0; i < M; i++)for (j = 0; j < N; j++)staticArray[i][j] = 0;
}void assign_array_cols_static() {int i, j;for (j = 0; j < N; j++)for (i = 0; i < M; i++)staticArray[i][j] = 0;
}void assign_array_rows_stack() {int i, j;short stackArray[M][N];  // 栈区数组for (i = 0; i < M; i++)for (j = 0; j < N; j++)stackArray[i][j] = 0;
}void assign_array_cols_stack() {int i, j;short stackArray[M][N];  // 栈区数组for (j = 0; j < N; j++)for (i = 0; i < M; i++)stackArray[i][j] = 0;
}void assign_array_rows_heap() {int i, j;short(*heapArray)[N] = (short(*)[N])malloc(M * sizeof(short[N]));  // 堆区数组for (i = 0; i < M; i++)for (j = 0; j < N; j++)heapArray[i][j] = 0;free(heapArray);
}void assign_array_cols_heap() {int i, j;short(*heapArray)[N] = (short(*)[N])malloc(M * sizeof(short[N]));  // 堆区数组for (j = 0; j < N; j++)for (i = 0; i < M; i++)heapArray[i][j] = 0;free(heapArray);
}double getElapsedTime(LARGE_INTEGER start, LARGE_INTEGER end, LARGE_INTEGER frequency) {return ((double)(end.QuadPart - start.QuadPart) / frequency.QuadPart) * 1000;
}void measure_time(void (*f)(), const char* desc) {LARGE_INTEGER start, end, frequency;double elapsed;QueryPerformanceFrequency(&frequency);QueryPerformanceCounter(&start);f();QueryPerformanceCounter(&end);elapsed = getElapsedTime(start, end, frequency);printf("%s: %f milliseconds\n", desc, elapsed);
}int main() {printf("M=%d, N=%d\n", M, N);measure_time(assign_array_rows_static, "Static - Rows");measure_time(assign_array_cols_static, "Static - Cols");measure_time(assign_array_rows_stack, "Stack - Rows");measure_time(assign_array_cols_stack, "Stack - Cols");measure_time(assign_array_rows_heap, "Heap - Rows");measure_time(assign_array_cols_heap, "Heap - Cols");return 0;
}

在这里插入图片描述

在这里插入图片描述

2.对实验结果进行分析,说明局部数据块大小、数组访问顺序等和执行时间之间的关系。

根据上面的数据,我们可以得出以下结论:

  1. 理论上:静态存储区按行访问比按列访问速度更快,这是因为静态存储区的元素是在内存中连续存储的,按行访问时可以利用CPU缓存的局部性原理,将靠近的元素一起读入缓存,提高访问速度。
    但是,实际上:按列赋值的时间更短,这其中可能的原因是:
    (1)编译器优化:不同的编译器可能对代码进行不同的优化。在这样的情况下,按列赋值的代码可能被编译器优化得更好,生成更高效的机器代码。
    (2)缓存局部性:按列赋值可能更好地利用了缓存局部性。由于内层循环按列访问数组元素,可能更接近数组在内存中的布局方式,从而更容易利用CPU的缓存机制,减少内存访问延迟。
    (3)并行计算:现代CPU通常具有多个处理单元,可以执行并行计算。按列赋值可能更容易实现并行计算,因为内层循环的迭代是独立的,可以同时进行多个迭代。

  2. 栈区比静态区和堆区快的原因主要有两个方面:
    (1)内存分配方式:栈区采用的是先进后出的方式进行内存的分配和释放,而静态区和堆区则是通过动态内存管理进行分配和释放。由于栈区的分配和释放操作相对简单,在编译时就确定了变量的生命周期和作用域,因此执行效率更高。
    (2)访问方式:栈区的数据访问是通过指针进行的,而静态区和堆区通过指针或引用进行访问。指针直接指向栈区的数据,访问速度更快;而静态区和堆区的数据需要通过指针或引用来间接访问,多了一层间接寻址的过程,相对较慢。
    需要注意的是,栈区虽然速度较快,但是其空间有限,内存分配和释放都是自动完成的,不适合存储大量的数据或者需要动态调整大小的数据结构。而静态区和堆区可以用于存储较大的数据或者需要动态管理的数据结构,但是其分配和释放操作相对复杂,效率相对较低。因此,在选择数据存储区域时,需要根据具体的需求综合考虑。

  3. 理论上:在堆区中,按行访问速度比按列访问速度稍快,这可能是因为堆区内存的分配方式导致元素不是完全连续存储的,按行访问可以利用CPU缓存的预取原理,将下一个元素提前读入缓存,避免了cache miss的情况。
    但是,实际上:按行与按列访问的速度差不多,这可能是因为编译器的优化。

  4. 在结合cache局部数据块大小、数组访问顺序等方面考虑,我们可以通过一些技巧来优化数组的访问效率,例如按照列访问时,可以将数组转置,使得元素在内存中更加连续,从而利用CPU缓存的局部性原理;另外,还可以利用循环展开、向量化等技术,使得CPU可以一次处理多个元素,提高访问速度。

(3)分析说明数组a分配在静态存储区、堆区和栈区,对for循环段的执行效率有没有影响。

根据上面的实验结果,我们可以看出:

  1. 静态存储区按列访问的时间最短,按行访问的时间稍长。说明数组在静态存储区按列存储时,访问效率较高。
  2. 栈区访问的时间最短,几乎可以忽略不计。说明数组在栈区分配时,访问效率最高。
  3. 堆区按行和按列访问的时间相差不大,但都比栈区慢,并且比静态存储区略慢一些。说明数组在堆区分配时,访问效率较低。

因此,从执行效率的角度考虑,数组在栈区分配时最优,其次是在静态存储区按列存储,最劣的是在堆区分配。但需要注意的是,数组的分配位置还应该考虑内存空间的大小和使用需求等因素,而不仅仅只考虑执行效率。

总结

通过对比不同存储区域(静态存储区、堆区和栈区)中数组的访问效率,分析了局部数据块大小、数组访问顺序等与执行时间之间的关系。实验结果表明,静态存储区按列访问最快,栈区访问速度最快,堆区访问相对较慢;同时,不同编译器、缓存机制等因素也会影响访问效率。因此,在选择存储区域时需要综合考虑执行效率、内存空间大小和使用需求等因素。

这篇关于【C语言】cache和程序访问的局部性对程序性能的影响的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/505655

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

安卓链接正常显示,ios#符被转义%23导致链接访问404

原因分析: url中含有特殊字符 中文未编码 都有可能导致URL转换失败,所以需要对url编码处理  如下: guard let allowUrl = webUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} 后面发现当url中有#号时,会被误伤转义为%23,导致链接无法访问

黑神话,XSKY 星飞全闪单卷性能突破310万

当下,云计算仍然是企业主要的基础架构,随着关键业务的逐步虚拟化和云化,对于块存储的性能要求也日益提高。企业对于低延迟、高稳定性的存储解决方案的需求日益迫切。为了满足这些日益增长的 IO 密集型应用场景,众多云服务提供商正在不断推陈出新,推出具有更低时延和更高 IOPS 性能的云硬盘产品。 8 月 22 日 2024 DTCC 大会上(第十五届中国数据库技术大会),XSKY星辰天合正式公布了基于星

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

EMLOG程序单页友链和标签增加美化

单页友联效果图: 标签页面效果图: 源码介绍 EMLOG单页友情链接和TAG标签,友链单页文件代码main{width: 58%;是设置宽度 自己把设置成与您的网站宽度一样,如果自适应就填写100%,TAG文件不用修改 安装方法:把Links.php和tag.php上传到网站根目录即可,访问 域名/Links.php、域名/tag.php 所有模板适用,代码就不粘贴出来,已经打