面试(三)

1. static关键字与const关键字作用

static
  1. static局部变量 将一个变量声明为函数的局部变量,那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中
  2. static 全局变量 表示一个变量在当前文件的全局内可访问
  3. static 函数 表示一个函数只能在当前文件中被访问
  4. static 类成员变量 表示这个成员为全类所共有
  5. static 类成员函数 表示这个函数为全类所共有,而且只能访问静态成员变量
const
  1. const 常量:定义时就初始化,以后不能更改。
  2. const 形参:func(const int a){};该形参在函数里不能改变
  3. const修饰类成员函数:该函数对成员变量只能进行只读操作
static关键字的作用:

(1)函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量和函数可以被模块内的函数访问,但不能被模块外其它函数访问;
(3)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(4)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

const关键字的作用:

(1)阻止一个变量被改变
(2)声明常量指针和指针常量
(3)const修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”。

参考:Static与Const的区别

2. 内联函数作用,为什么比普通函数快

  • 内联函数与普通函数的区别内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避 免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通 过编译器控制来实现。
  • 内联函数的执行过程与带参数宏定义很相似,但参数的处理不同。带参数的宏定义并不对参数进行运算,而是直接替换;内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算求值,然后把表达式的值传递给形式参数。
  • 内联函数与带参数宏定义的另一个区别是,内联函数的参数类型和返回值类型在声明中都有明确的指定;而带参数宏定义的参数没有类型的概念,只有在宏展开以后,才由编译器检查语法,这就存在很多的安全隐患。
    使用内联函数时,应注意的问题:
      1)内联函数的定义性声明应该出现在对该函数的第一次调用之前。
      2)内联函数首先是函数,函数的很多性质都适用于内联函数,如内联函数可以重载。
      3)在内联函数中不允许使用循环语句和switch结果,带有异常接口声明的函数也不能声明为内联函数。

宏和函数的区别:
  1. 宏做的是简单的字符串替换(注意是字符串的替换,不是其他类型参数的替换),而函数的参数的传递,参数是有数据类型的,可以是各种各样的类型。
  2. 宏的参数替换是不经计算而直接处理的,而函数调用是将实参的值传递给形参,既然说是值,自然是计算得来的。
  3. 宏在编译之前进行,即先用宏体替换宏名,然后再编译的,而函数显然是编译之后,在执行时,才调用的。因此,宏占用的是编译的时间,而函数占用的是执行时的时间。
  4. 宏的参数是不占内存空间的,因为只是做字符串的替换,而函数调用时的参数传递则是具体变量之间的信息传递,形参作为函数的局部变量,显然是占用内存的。
  5. 函数的调用是需要付出一定的时空开销的,因为系统在调用函数时,要保留现场,然后转入被调用函数去执行,调用完,再返回主调函数,此时再恢复现场,这些操作,显然在宏中是没有的。

内联函数

  • 所谓“内联函数”就是将很简单的函数“内嵌”到调用他的程序代码中,只样做的目的是为了避免上面说到的第5点,目的旨在节约下原本函数调用时的时空开销。但必须注意的是:作为内联函数,函数体必须十分简单,不能含有循环、条件、选择等复杂的结构,否则就不能做为内联函数了。事实上,即便你没有指定函数为内联函数,有的编译系统也会自动将很简单的函数作为内联函数处理;而对于复杂的函数,即便你指定他为内联函数,系统也不会理会的。

参考:内联函数与普通函数的区别

  1. 内联函数和普通函数的参数传递机制相同,但是编译器会在每处调用内联函数的地方将内联函数内容展开,这样既避免了函数调用的开销又没有宏机制的缺陷

  2. 普通函数在被调用的时候,系统首先要到函数的入口地址去执行函数体,执行完成之后再回到函数调用的地方继续执行,函数始终只有一个复制。

    内联函数不需要寻址,当执行到内联函数的时候,将此函数展开,如果程序中有N次调用了内联函数则会有N次展开函数代码

  3. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句。如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行

参考:内联函数和普通函数的区别

3. 代码优化

  • 所谓代码的优化,个人认为有三个方面:更快,更省,更好看。快指的是时间少,省指的是省空间,好看指代码简洁。这三者有时候会有冲突,而我所追求的则是达到三者的平衡,有时甚至可以兼顾三者。

利用计算机原理优化

  • 其核心在于利用计算机的金字塔物理结构,提高运算效率。CPU运算速度非常快,但数据在内存或者磁盘上,而计算通常都是发生在CPU,数据也并非直接送入CPU,而是经过多级传递,所以重点在于CPU每次计算时,数据都刚好在“需要的地方”。这里可以存在的优化有:

读写文件优化

  • 通常网上教读写文件的方式是利用fstream,将文件转化为数据流,之后再按照数据类型,一个个地读入和转化数据,这里的优化就可以先将所有的数据读入到内存,之后再进行数据的转换。两种代码如下:
//逐个读
int n;
ifstream infile(path);
infile>>n;
//一起读
int n;
ifstream fin(path,std::ios::binary);
std::vector<char> buff=vector<char>(fin.seekg(0,ios::end).tellg());
fin.seekg(0,ios::beg).read(&buff[0],static_cast<std::streamsize>(buff.size()));
fin.close();
stringstream infile;
std::copy(buff.begin(),buff.end(),std::ostream_iterator<char>(infile));
infile>>n;
std::vector<char>().swap(buff);//释放内存

提前分配

  • 上述文件转化代码,一口气将数据读入内存,虽然会有加速,但影响更大的其实在于可以根据读入内容,提前分配空间。对于一个数组的新建元素,可以使用push_back,但若可以提前预知大小,先分配空间,再来填元素,在大型数据上,将产生数量级的时间差。

  • 对于以上读文件数据代码,下一步优化空间在于提前根据数据内容,分配好空间。更进一步,则是限制输入数据的格式,批量转化。在实验中,反复读取的数据,也可以通过此类型的预处理序列化存储好,可以节约大量时间。

减少cache miss

  • 减少cache miss就是当CPU在计算的时候,需要的变量都在cache里。这件事其实对人脑也适用,例如大部分人其实都不太能一心两用,更不必说反复切换。如果编写了一分钟c++代码,又迅速切换到python,再迅速切换到java,这样有可能会造成用法的混乱,但若是一个月都在编写c++,那么这个月写c++都是比较通畅的,再切换python,虽然要几天适应,但不用太久又能很熟练地使用。
  • 要想减少cache miss就需要注意对数据结构的连续设计,同时声明和引用不能距离太远,这些算是比较细节的问题。

图的存储

  • 通常图有两种存储方式,邻接矩阵和邻接表,邻接矩阵基本不用(这里指稠密版,稀疏版其实就是邻接表),在c++上,大部分人都是自己定义图的存法,但在python上,使用的有coo matrix,csr matrix,他们本质上都不是“链表式”邻接表,而是数组式的存储。链表的问题就在于,分配元素时,所占的空间并不连续。当将变量放入cache时,通常都是按块往里放。例如需要读取某个图节点的所有邻居,假使用csr、coo的存法,一口气就可以把好几个邻接的坐标读进去,因为他们连续分配,但对于链式存储,那将可能每次只能读进去几个邻居,由此必然导致新的io,导致速度太慢。

矩阵存储与计算

在这里插入图片描述

空间分配

  • 虽然图用csr存很方便,但是毕竟不是链式邻接表,它插入元素有一个缺点,插入元素时,它无法一直保持原来的结构。所以构造图时,通常都是先生成coo 三元组(u,v,e),再插入图中。例如我最常用的Eigen库,若在构造了Sparse Matrix之后,再插入元素,所消耗的时间有时甚至可能比重新构造三元组再插入的要多,这和哈希表的构造有异曲同工之妙,哈希表通常会多预留一倍的空间,当插入的元素太多时,则批量重构哈希表,虽然这里的重构原因不一定一样。这就是编程和人生的常态吗,随着一个系统累积的毛病越来越多时,加补丁吃药亡羊补牢都是于事无补,最终还是看有没有将一切推倒重来的勇气,往往最后才能获得涅槃重生。

引用与指针

  • 在c++这种“优秀”的语言下,最“好用”的技能当属指针了。其实我是特别不喜欢用指针,指针经常会出现莫名其妙的错误,犯这些错误通常在于,搞不清楚什么时候分配,什么时候回收,不知道写中止符,造成内存泄露和非法访问。但也只有用好指针才能勉强算是会c++,别的不说,我最常使用的是用指针传递数据。在没有指针的语言里,很多时候“=”都代表复制构造,会将所有值都进行复制,这样带来的是大量时间开销,弄不清指针时,最好的办法是使用引用,传递地址,而不是新建变量浪费空间。虽然这件事也不是绝对的,当需要备份或对两个变量进行操作时,还是只能复制。

利用别人的包优化

  • 人力有尽时,大部分情况下,别人写的包都是优于自己随手写的代码的,除非算法不一样,否则按照底层优化来说,包通常都会尽可能优化的,只需要注意是否开源,然后用即可。以下将谈谈我对一些矩阵运算库的理解,这里倒是没有做过对比实验,完全基于我自己使用的感觉,同一个库,不同人用的优化层次是不太一样的,还是根据需求选择比较好。

矩阵运算库

Armadillo

  • Armadillo 算是我使用的第一个c++矩阵计算库,优点胜在语法简单,接近 Matlab,速度其实也还好,关键看用了什么blas。其实这些开源的矩阵运算库,可能是由于经费的原因,代码还是比较朴素的,优化的层次比较低,实际上还是在调用各种矩阵运算,只是一个壳而已。而这些壳可以套不同的线性代数库 BLAS(basic linear algebra subroutine) ,BLAS是一种接口的标准,而不是某种具体实现,具体实现在不同版本下带来巨大的速度差异,CPU下性能最好的,个人感觉是 Intel 开发的 MKL,简直是将 CPU 利用到了极致。这也就意味着Armadillo可以用MKL,也可以用各种 BLAS,从这个角度来说,使用GPU也是可以的,所以水平够的话,何必额外套层壳呢?直接用底层的库不香吗?

MKL(Intel Math Kernel Library)

  • MKL,Intel开发的矩阵运算库,用起来是真的香,速度也是真的快,但是要用得好,就必须好好看文档。在国内资料少的情况下,学和用也是真的难,但更难的还是配置MKL环境。以学生身份注册可以有一年免费使用完整版的机会,但这样配置起来还挺麻烦的,踩了不少坑,这里也就不说了,后来过期了是真的难受,不得不用了简化版,最核心的是 Intel 的编译器 icc 用不了。

  • 说到科技被卡脖子,优化真的不能做的太好,要做到极致依赖就会比较多。有位老师说的好,别人的软件或者代码升级并不一定意味着是往你所想要的方向升级,有可能安全性更高,准确性越高,但是所需要的效率就可能降下来了。正如5G虽然升级了,但是实际并未能符合人们的期望,人们想要的是更稳定,更快,更便宜的网络,但显然兼顾方方面面是没那么容易的。

  • MKL为什么快,可以从CPU的使用情况感受到,但是其内部做了什么优化,并不是如今我的层次所能理解背后原因的,但重要吗?重要的其实只在于用就好,就像 python 众多包一样,没多少人会去看底层的代码,好用就完事了,但 MKL 实际并不那么好用,因为函数传递的往往都是指针数组,这就回到了为什么入门最好用Armadillo的原因,MKL快是真的快,但写完程序全是 bug,那能力不行的就直接劝退了不是?

  • MKL的long使用还是我心头一根刺,有待其他研究的同学一起讨论。

Eigen

  • Eigen 常有博客将这三者比较,但个人感觉三个都可以用,看个人习惯。Eigen和Armadillo一样,都是可以借用MKL加速优化的,甚至由于开源,我也尝试过,直接改 Eigen 源码搭载MKL的接口,速度可以提升不少。Eigen不够快时可以从CPU的使用情况看出原因,凡MKL一运行,大部分时候,所有CPU的核都会用上,但Eigen通常不会。

  • 我个人通常在 R语言里使用c++时,会优先考虑Armadillo,在python 中使用c++时会考虑Eigen,因为有对应的库。但是R里也有RcppEigen,python 也能用Armadillo。说到底还是一个习惯问题,人往往有先入为主的概念,当阅读的论文和代码都是用某一种库时,自然而然的就会用那个库,毕竟改起来复现实验容易,复现代码很多时候并不是那么简单的事。

  • Eigen 在不同人的手里也会导致很大的速度区别,这些优化Eigen的手段可以在知乎上搜到,最常见的可以有,当生成变量不为输入变量时,可以将普通乘法用以下命令,加快速度:

M.noalias()=A*B;
  • 其他优化的手段还有例如向量对齐、加附加的优化参数、提前声明静态的矩阵类型。这些优化都不仅仅是在 Eigen 中使用的,其他库也有相应的优化,需要具体情况具体分析,说到底还是一个编程经验。

  • 说到具体情况具体分析,不同的库对于不同运算的支持能力会不一样,例如Eigen做LU分解比较难,特别是矩阵不是正方形时。

随机数生成

  • 这也是看别人论文+代码学会的,自己生成随机数的算法:http://xoroshiro.di.unimi.it/#shootout

  • 复现别人代码的过程也是学习的过程,吃透越多代码,消化越多,最终转化为的是个人能力,本文的大部分内容,也是这一两年来,我从这些浩瀚的文章代码中提炼出的优化手段。但部分代码至今都还未能消化,这也是水平所限。好在,我们想要用这些很高级的优化算法和方法,很多时候都不需要自己实现,把自己需要的部分剥离出来即可。

编译优化

  • 编译的命令会很大程度影响速度,最常见的例子是,在c++编译时加上 -O3,将会对速度有很大提升,这份提升是编译带来的,可想而知,MKL不能用icc编译,必然效果也会差一些。下面是一些编译上优化的例子。

  • 这里其实也是想说,虽然高级语言大家都可以用,但是编译是可以有不同编译的手段和写法的。似乎比较少知道有实验室或者是组是做编译优化的,虽然有这样的课,但总感觉教得比较浅显,学的也比较浅显。也不知道编译器的研究有没有列入什么研究清单。要是能开发出国内开源的强力编译器,那我认为值得充钱一用(借壳改名不可取)。

Eigen的附加参数

-mavx -mfma

Armadillo

  • 可以通过改变编译参数,使用不同的blas,这点可以看一下文档。使用不同blas命令不同,而且需要引入不同的路径,其实还挺麻烦的。

MKL

  • 不能用完整版MKL时,需要用特殊的g++编译命令,引入路径,非常复杂,而且没有很明确的编译命令,甚至在linux和mac上不一致,顺序也会影响,在我多次尝试之后,命令如下:
mac
g++ main.cpp -std=c++11 -I/opt/intel/mkl/include -I/opt/intel/mkl/lib/intel64 -L/opt/intel/mkl/lib -lmkl_intel_lp64 -lmkl_intel_thread -lmkl_core -I /opt/intel/compilers_and_libraries_2019.3.199/mac/mkl/include -L /opt/intel/compilers_and_libraries_2019.3.199/mac/mkl/lib -L /opt/intel/compilers_and_libraries_2019/mac/lib
linux ubuntu
clang++ main.cpp -std=c++11 -I/opt/intel/mkl/include -I/opt/intel/mkl/lib/intel64 -I/opt/intel/lib/intel64 -lmkl_intel_lp64 -lmkl_core -lmkl_intel_thread -L/opt/intel/mkl/lib -I /opt/intel/compilers_and_libraries/linux/mkl/include -L /opt/intel/compilers_and_libraries/linux/mkl/lib -L /opt/intel/compilers_and_libraries/linux/lib -liomp5 -lpthread
  • 以上参数有可能有一些冗余,而且需要调整,不适用于每个电脑和服务器,这也是我平常非常不喜欢用MKL的原因之一,实在是太麻烦,太恶心人了,换个服务器就需要再不断地尝试,十分绝望,好在还有下一招,就是写CMakeLists。但经过我个人实验时的感觉来说(我已经不能再测试icc了,无法再比较)即使是使用CMakeLists,也没有icc快。

CMake

  • 直接写CMakeLists.txt,然后使用find的方法,找到mkl,并加上编译参数,基本可以换环境后,还能运行。使用cmake还可能会提升一定的运行速度,当基本项目框架一致时,熟练使用CMakeLists可以很好完成代码的升级和迭代(就是不断往里面套用新的库)

并行优化

待补充。

openmp

  • 使用openmp优化,其实就是加一条引用omp的参数,再往c++代码中加一行并行的代码,网上资料比较多,这里并没有必要详细给例子。

thread

  • 使用c++的多线程库thread,难点可能在于加锁等问题上,而我也会皮毛,也就把一个for循环,按照核的多少进行拆分。之后是一个可以深入学习的部分,再加入此文章中。

MKL & CUDA

  • MKL确实是能利用多核CPU并行最佳的方法之一。CUDA则是使用GPU。
  • 以上,并行提速能做的事很多,但还不是我如今的境界所能掌控的,未来需要提升的还很多,可能将是小结系列二的内容。

算法优化

  • 知识就是力量。算法有用吗?视情况而定,重点在于瓶颈在哪。有一种观点是,算法没啥用,差不多就好,堆算法太慢,堆钱快,速度不够快,内存不够大,用钱堆GPU,堆内存,堆磁盘,堆设备就好,但是当一个代码在几十上百核的服务器运行的时间,还没有换一个算法,在普通个人计算机上计算效果要好,速度要快时,我不禁想问,这些钱用在刀刃上了吗?文章开篇所说,时间、空间、代码美观度,三者若要同时达到最佳,那必须要有一个相对最好的算法,这是刨除外物之后,能力的最深层表现。

矩阵结构

  • 可以优化的矩阵结构通常两种,稀疏或低秩。也算是我核心研究内容,涉及的算法远不是一篇小文能讲清,知道好用即可。

空间换时间

  • 不能达到时间空间兼顾时,由于空间便宜,时间宝贵,通常都是采取时间换空间的手段。

Alias Method

Sqrt

  • 求平方根并不是空间换时间的优化方法,这里是一个引子,想说越基础的操作,速度可以是瓶颈。传说有一个神奇的值0x5f3759df,没人知道这个值是从哪里来的,但可以快速计算平方根。我们刚开始编程所熟知的方法是二分查找,实在太慢。连分数法也是一种方法。还可以空间换时间。最后是利用神奇的值的快速开方算法。可以参考博客开平方的七种算法我也是偶然看到的。

  • 时间换空间固然美,但最优解确是两者兼顾。确实是妙不可言。看起来这样简单的问题没有什么很大提升,但实际复杂的问题往往由简单的问题构成。每一点提升都至关重要,在没有这样最优算法时,往往能做的就只有空间换时间,例如机器学习中的激活函数Sigmoid。

Sigmoid

  • Sigmoid函数不算复杂,但如果不从基础函数定义,是否能加速呢?目前我看到的方法就是以空间换时间。由于sigmoid函数自身特点,往外延伸,很快接近0,1,在一定范围之后,直接将值赋值为0,1即可,在此范围内,则可以划分为n个小区间存好,之后求值就成为了单次读取即可得到值的算法。

  • 以上都是非常简单的例子,像这样的基础算法实在太多,例如Partial Sum、桶排序等等。往往我们都只需要将这件事放在心上,看情况去使用即可。

代码优化

  • 多读,多写,多重构,就会感觉到以前写的代码有多垃圾。

c++11
lambda表达式

  • 大部分编程语言都能用,用了就短,短就简洁,缺点是,别人可能看不懂。

auto

  • 避免迭代变量赋值,当模版套模版时,使用起来其实还挺方便的。

批量操作

  • 以前写R语言时,默认观点时减少写for循环,R不写for循环可以很大程度提高速度,c++则不一定,但至少代码是短的。说起来容易,做起来总是难的,也挺看个人经验。

参考:代码解构|C++代码的优化小结系列一

4. 缓存机制cache

  • CPU缓存有多级缓存,比如L1, L2, L3等:
  • L1容量最小,速度最快,每个核都有L1缓存,L1又专门针对指令和数据分成L1d(数据缓存),L1i(指令缓存)。
  • L2容量比L1大,速度比L1慢,每个核都有L2缓存。
  • L3容量最大,速度最慢,多个核共享一个L3缓存。
    在这里插入图片描述
    在缓存和主存之间,数据是按固定大小的块传输的 该块称为缓存行(cache line)。
    在这里插入图片描述
    cpu从来都不直接访问内存, 都是通过cache间接访问内存,每次访问内存时先查看cache中是否有对应地址的数据,如果cache中没有则先分配一个cache entry,然后再把内存中的数据copy到刚刚分配的cache entry的cache line中,再从cache line中读取数据。

缓存性能的10种高级优化方法

在这里插入图片描述
多核CPU,每个核都拥有独立的L1/L2的缓存,而缓存按照最小单位缓存行进行和内存交互,缓存行一般为64字节。

可能优化的点
分块的大小与cpu cache line关系?
如果我们访问内存时,先把数据读取到CPU缓存cache line再计算,而下次读取到该数据时直接使用缓存(若未被淘汰掉),修改cpu cache line这个数值,数据字节对齐可能决定一次操作接触1个还是2个缓存行

避免伪共享的问题?
当多线程修改互相独立的变量(一般出现在全局变量或者一些动态申请的内存)时,如果这些变量共享同一个缓存行,那么当某个核修改缓存行某个变量值时,需要锁定缓存行,同时其他核已经加载该缓存行的cache也跟着失效,需要重新从内存读取。提到 cache line 往往会提到伪共享,也就是多核 CPU 的多线程编程中,每个线程虽然都访问自己本地的变量,没有使用任何线程同步锁,但由于这些变量在一个 cache line 上,也会造成 cache miss,导致性能严重下降。可以通过内存对齐使多线程之间不共享 cache line,以达到优化的目的。其中一个解决思路,就是让不同线程操作的对象处于不同的缓存行即可。

参考:c/c++性能优化— cache优化的一点杂谈
参考:c++缓存优化

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落花逐流水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值