【嵌入式开发学习】__改了一行代码,数组遍历耗时从10.3秒降到了0.5秒!

本文主要是介绍【嵌入式开发学习】__改了一行代码,数组遍历耗时从10.3秒降到了0.5秒!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

前言

两个简单的测试程序

1. array1.c 对数组按行进行访问

2. array2.c 对数组按列进行访问

​编辑 3. 编译运行,并用 time 命令统计一下运行时间:

重要说明

存储金字塔

局部性原理

1. 时间局部性

2. 空间局部性

 高速缓存 - Cache

Cache Line

Cache Line 实例讲解

按行、列访问的真正差异 - Cache

用perf观测性能指标

小结

(* ̄︶ ̄)创作不易!期待你们的 点赞、收藏和评论喔。


前言

本专题除讲解常见性能问题以及分析、调优手段外,还会重点讲解一些对系统性能至关重要,却又经常被忽视的高级话题,如Cache、指令流水线、Superscala、SIMD、分支预测、内存屏障等。此外,还会涉及到编译器、操作系统等话题。

两个简单的测试程序

定义一个同样大小的二维数组,然后循环遍历,对数组元素赋值。

1. array1.c 对数组按行进行访问

2. array2.c 对数组按列进行访问

 3. 编译运行,并用 time 命令统计一下运行时间:

array1用时0.528秒,array2用时10.310秒。array2耗时居然是array1的将近20倍

有没有被这个结果震惊到?为什么会有如此之大的性能差异呢?

重要说明

要想真正理解这个问题,必须要先补充一些关于现代计算机存储系统相关的背景知识,这也是理解这个问题的关键所在。为方便大家理解,我会尽量以白话的形式进行讲解,尽可能避免枯燥无味的纯理论描述。

存储金字塔

相信不少人都听过“存储金字塔”这个词,或者至少见过类似下面这张图:

这张图很直观地描述了现代计算机系统的分级存储模型

可以认为CPU就位于金字塔的顶点上,越靠近塔顶,离CPU越近,访问速度越快,但生产成本越高,相应的容量也就越小。在所有存储器中,寄存器直接内嵌在CPU中,访问速度最快,容量也最小,一般CPU上也就最多几十个通用寄存器供应用程序使用。

反之,越往下靠近塔底,访问速度越慢,生产成本越低,相应的容量也就越大。比如图中最底部的网络存储设备,相对其他存储设备而言是访问速度最慢的,但其容量却几乎可以认为是无限制的。

那么,这种金字塔式结构中,不同层级的存储设备之间究竟是如何协调工作的呢?

用一句话概括

高一级的存储设备,往往是作为低一级存储设备的缓存来使用的。

简单来说,系统运行时,为了提升数据访问效率,把程序中最近最经常访问的数据,尽可能放到访问速度更快的高一级存储器中。这样一来,每次访问数据时,从金字塔的顶端开始,都先尝试在高一级存储器中查找,如果被访问的数据存在且有效,则直接访问,否则,就逐级到更低级的存储器中去查找。

这种金字塔式的分级存储模型之所以能够以近乎完美的方式运行,实际上都是建立在现代计算机科学中的一个非常重要的理论基础之上:程序的局部性原理

 

局部性原理

一个程序的局部性,包含两个维度:时间局部性空间局部性

1. 时间局部性

如果一个数据在某个时间点被CPU访问了,那么在接下来很短的一段时间内,这个数据很有可能会再次被CPU访问到。

2. 空间局部性

如果一个数据在某个时间点被CPU访问了,那么与这个数据临近的其他数据,很有可能也会很快被CPU访问到

 高速缓存 - Cache

根据常识,我们知道,程序要想被CPU正常执行,必须要先被加载到内存中

但是,内存的访问速度与CPU运行速度相比,要慢好几个数量级,可以想象一下,如果CPU每次都直接从内存中存取数据,会造成大量的计算资源浪费,程序性能严重受损,假如真是这样的话,你还能像现在这样愉快的吃鸡吗?

为了解决CPU和内存之间速度严重不匹配的问题,在CPU和内存之间设计了高速缓存,即Cache。

目前,主流CPU一般都有三级(或更多级)的Cache,依次是L1 Cache、L2 Cache、L3 Cache。其中L1 Cache速度最快,几乎可以和内嵌在CPU中的寄存器媲美,容量也最小,而L3 Cache速度最慢(但仍然比内存快很多),但容量最大。

CPU读取数据时,会在L1、L2、L3Cache中逐级查找,如果找到,就从Cache直接读取,找不到再从内存读取,并且把数据存放到Cache中,以便提高下次访问的效率。

在这个过程中,如果在Cache中找到所需数据,称为Cache命中(Cache Hit), 找不到称为Cache未命中(Cache Miss)

不难看出,L1 Cache命中的时候,读取数据最快,性能最好,而当L1、L2、L3 Cache全部未命中时,就必须要直接从内存中读取数据,性能最差!

Cache Line

Cache Line 是 Cache和内存之间进行数据传输的最小单位。

根据上文讲解的程序的局部性原理,如果一个数据被CPU访问了,那么这个数据相邻的其他数据也很快会被访问到。因此,为了提高内存数据的读取效率,并且最大化利用CPU资源,数据在Cache和内存之间传输时,不是一个字节一个字节进行传输的,而是以缓存行(Cache Line)为单位进行传输的。

不同CPU的Cache line大小可能不同,典型的CPU Cache line大小是64个字节。

我们通过下面一个简单的例子,加深一下理解。

Cache Line 实例讲解

在一个Cache Line大小为64字节的机器上,定义个数组:

int a[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};

我们假设数组a的起始地址是Cache Line对齐的,可以简单理解为a的地址能被64整除。假设,数组a还从来没有被访问过,如果此时需要访问a中的一个元素a[5],如:

int x = a[5];

由于在此之前数组a没有被访问过,所以理论上讲,数组a应该只存在于内存中,并未被加载到Cache中。因此,此时CPU在Cache中找不到a[5],触发Cache Miss,然后需要从内存中读取数据,并更加载到Cache中。前面提到,Cache和内存之间是以Cache Line为单位进行数据传输的,因此,这里会把一个Cache line大小(64字节)的数据从内存读取出来加载到Cache中。由于a的起始地址恰巧是Cache line对齐的,所以CPU会把整个数组(64个字节,刚好一个Cache Line)都加载到Cache中。

紧接着,如果再访问数组a的元素,如:

int y = a[10];

此时,整个数组都在Cache中,所以CPU在访问时,触发Cache Hit,直接从Cache读取数据即可,不需要再从内存中读取。

了解了Cache的背景知识,现在来分析下array1.c和array2.c为什么会存在这么巨大的性能差异。

按行、列访问的真正差异 - Cache

首先,必须要知道一点:C语言的数组,所有元素是存放在地址连续的内存中的,此外,C语言的多维数组,是按行进行存储的

array1.c按行对数组进行访问,也就是从数组起始地址开始,一直连续访问到数组的最后一个元素的地址处。第一次访问一个Cache Line的首个元素时,触发Cache Miss,与该元素临近的几个元素会组成一个Cache Line,被一起加载到Cache中。如此,在访问下一个元素的时候,就会Cache Hit,直接从Cache读取数据即可。

而array2.c按列对数组进行访问,因此并不是按照连续地址进行访问的,而是每次间隔10240 * 4个字节进行访问。第一次访问一个Cache Line的首个元素时,假设地址为x,尽管该元素临近的一个Cache Line大小的元素也会被一起加载进Cache中,但是程序接下来访问的并不是临近的元素,而是地址为x + 10240 * 4处的元素,因此会再次触发Cache Miss。而当程序回过头来访问x + 4地址处的元素时,这个Cache Line可能已经被其他数据冲刷掉了。因为,尽管Cache会尽量缓存最近访问过的数据,但毕竟大小有限,当Cache被占满时,一些旧的数据就会被冲刷替换掉。

可以看出,无论是时间局部性还是空间局部性,array1.c都要比array2.c好很多!相比array1.c,array2.c会触发大量的Cache Miss,这也是为什么array2的性能会如此之差!

下面我们用perf来对比下这两个程序的性能指标。

用perf观测性能指标

perf是Linux提供的一个功能强大的实用性能调优工具,它可以用来观测几乎所有CPU相关的性能指标,但perf工具本身,不是本文重点,以后会有专门文章详细介绍perf的使用。

1. 先获取array1.c的性能指标:

2. 再看下array2.c的性能指标:

这里,我们只关注一个最重要的性能指标:L1 Data Cache的Miss率。

对比一下,array1.c的L1-dcache-load-misses率只有3.10%, 而按列访问的array2.c,则达到了惊人的93.03%!这就是两者性能差异如此巨大的真相!

小结

Cache是CPU内部最为重要的部件之一,也是影响程序性能的最重要因素之一,但由于各种原因,经常被程序员忽视。在进行性能调优时,除了系统架构、算法等方面的考量之外,不妨也从CPU的角度去思考一下,比如Cache,有时会对程序性能产生决定性影响。


(* ̄︶ ̄)创作不易!期待你们的 点赞收藏评论喔。

本文来源网络,免费分享知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除!

这篇关于【嵌入式开发学习】__改了一行代码,数组遍历耗时从10.3秒降到了0.5秒!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Python38个游戏开发库整理汇总

《Python38个游戏开发库整理汇总》文章介绍了多种Python游戏开发库,涵盖2D/3D游戏开发、多人游戏框架及视觉小说引擎,适合不同需求的开发者入门,强调跨平台支持与易用性,并鼓励读者交流反馈以... 目录PyGameCocos2dPySoyPyOgrepygletPanda3DBlenderFife

使用Python开发一个Ditto剪贴板数据导出工具

《使用Python开发一个Ditto剪贴板数据导出工具》在日常工作中,我们经常需要处理大量的剪贴板数据,下面将介绍如何使用Python的wxPython库开发一个图形化工具,实现从Ditto数据库中读... 目录前言运行结果项目需求分析技术选型核心功能实现1. Ditto数据库结构分析2. 数据库自动定位3

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

Django开发时如何避免频繁发送短信验证码(python图文代码)

《Django开发时如何避免频繁发送短信验证码(python图文代码)》Django开发时,为防止频繁发送验证码,后端需用Redis限制请求频率,结合管道技术提升效率,通过生产者消费者模式解耦业务逻辑... 目录避免频繁发送 验证码1. www.chinasem.cn避免频繁发送 验证码逻辑分析2. 避免频繁

精选20个好玩又实用的的Python实战项目(有图文代码)

《精选20个好玩又实用的的Python实战项目(有图文代码)》文章介绍了20个实用Python项目,涵盖游戏开发、工具应用、图像处理、机器学习等,使用Tkinter、PIL、OpenCV、Kivy等库... 目录① 猜字游戏② 闹钟③ 骰子模拟器④ 二维码⑤ 语言检测⑥ 加密和解密⑦ URL缩短⑧ 音乐播放

Spring Boot集成/输出/日志级别控制/持久化开发实践

《SpringBoot集成/输出/日志级别控制/持久化开发实践》SpringBoot默认集成Logback,支持灵活日志级别配置(INFO/DEBUG等),输出包含时间戳、级别、类名等信息,并可通过... 目录一、日志概述1.1、Spring Boot日志简介1.2、日志框架与默认配置1.3、日志的核心作用

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

SpringBoot监控API请求耗时的6中解决解决方案

《SpringBoot监控API请求耗时的6中解决解决方案》本文介绍SpringBoot中记录API请求耗时的6种方案,包括手动埋点、AOP切面、拦截器、Filter、事件监听、Micrometer+... 目录1. 简介2.实战案例2.1 手动记录2.2 自定义AOP记录2.3 拦截器技术2.4 使用Fi

PyQt5 GUI 开发的基础知识

《PyQt5GUI开发的基础知识》Qt是一个跨平台的C++图形用户界面开发框架,支持GUI和非GUI程序开发,本文介绍了使用PyQt5进行界面开发的基础知识,包括创建简单窗口、常用控件、窗口属性设... 目录简介第一个PyQt程序最常用的三个功能模块控件QPushButton(按钮)控件QLable(纯文本

Python实现MQTT通信的示例代码

《Python实现MQTT通信的示例代码》本文主要介绍了Python实现MQTT通信的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 安装paho-mqtt库‌2. 搭建MQTT代理服务器(Broker)‌‌3. pytho