在观看了 Cherno 的 C++ 系列视频后的一些记录——技巧篇,囊括了有关 C++ 的性能操作、功能性操作
2024-07-27 11:06:29
目录:
std::vector
的优化使用
我们创建类对象时,实际上是在主函数的当前栈帧中构造它,因此需要把类对象从mian
函数中复制到vector
类中。这是可以优化的第一件事,在vector
分配的内存中构造类对象
1 | std::vector<Vertex> vertices; |
提前告知需要的内存大小(2个对象),这样就不必调整大小了,这就是第二种优化策略
1 | std::vector<Vertex> vertices; |
创建与使用库
如何处理多返回值
堆与栈内存的比较
当我们在栈中分配变量时,发生的是栈指针基本上移动变量大小的字节,如果我们想分配一个4个字节的整数,就把栈指针移动4个字节。栈的做法只是把东西堆在一起,这就是为什么栈分配非常快,它就像一条 CPU 指令,我们所做的就是移动栈指针,然后返回栈指针的地址
new
关键字实际上调用了malloc
函数,当你使用malloc
请求堆内存时,它可以浏览空闲列表(跟踪哪些内存块是空闲的),然后找到一个至少和你要的一样大的空闲内存,做一堆记录,然后返回给你一个指针。更麻烦的是如果你想要更多的内存,超过了空闲列表,超过了操作系统给你的初始分配,会有一堆事情
在堆上分配内存是一堆的事情,而在栈上分配内存就像一条 CPU 指令,这是这两种主要的内存分配方法的区别
为什么不使用using namespace std
以下是几个原因:
- 命名冲突:如果两个命名空间中有同名的函数或类,使用
using namespace std
可能会导致编译器混淆,不知道你想要引用的是那个名称。这会导致编译失误或意外的行为 - 命名空间污染:当你在全局范围内使用
using namespace std
时,它会使用命名空间中的所有名称都成为全局变量。这可能导致与其他代码库发生冲突,这些代码库可能没有意识到你正在使用标准库中的名称
如果你的头文件(.h
、.hpp
)有被外部使用,则不要使用任何 using 语句引入其他命名空间或其他命名空间中的标识符。在源文件(.cpp
)中用 using 语句就是个人的选择了,如果你决定在代码中使用 using namespace std
,最好将其限制在一个函数、类或命名空间内。这样可以减少对其他代码的影响,并减少潜在的命名冲突。
综上所述,通常建议的做法是明确指定要使用的标准库名称。这样做的好处是代码更加清晰和可维护,并且减少了潜在的命名冲突和错误。
线程
计时
计时对很多事情都很有用,不管你是希望某些事情在特定时间发生,还是只是评估性能或做基准测试——看看你的代码运行得有多快,你需要知道应用程序实际运行的时间。有几种方法可以实现这一点,C++11 之后,我们有 chrono,它是 C++ 库的一部分,不需要去使用操作系统库,但在有 chrono 之前,如果你想要高分辨率的时间(一个非常精准的计时器),那你需要使用操作系统库。事实上,如果你想要更多地控制计时,控制 CPU 的计时能力,那么你可能会使用平台特定的库(如 Windows 中的 QueryPerformanceCounter)。chrono 是与平台无关的 C++ 标准库方法,这是一种非常简单的方法,因为并不是所有的平台都有足够好的,不增加开销的分析工具。
代码实现如下:
1 | class Timer { |
类型双关
我们可以用不同的方式(转换成指针)解析一段内存,从而得到不同的结果,类型只是约定的解析内存的方式
条件与操作断点
预编译头文件
基准测试
当你正在处理一个对性能相当关键的部分,或者你正在测试你刚刚学过的新技术,你想把它的性能和过去使用的方法做个比较,看看那个性能好。有些人喜欢依赖第三方工具,有些人喜欢对代码进行仪表化处理,给它们设置计时器之类的东西,有些人喜欢运行他们的程序,把程序封装在一个计时器中,然后测试特定的程序。
值得注意的是,无论你在测试什么,都需要确保你确实做了这些事情,因为编译器实际上会非常积极地改变你的代码,具体可见
如何让 C++ 运行得更快
我们会讨论如何通过多线程来提高性能,这是为现代硬件而设计的,因为它们能够并行处理。它们有多个 CPU 核心,这意味着可以在同一时间并行执行指令,而不需要等待上一条指令完成后再执行下一条 CPU 指令。值得注意的是,做并行运行最难的是要找出彼此的依赖关系,并想清楚在不同的线程中放什么
正如你所看到的,这种多线程对你的程序是非常有益的,你可以通过充分利用你的硬件来提高速度。很多优化,很多性能方面的东西都是关于充分利用你正在工作的硬件。知道你计划在什么平台上发布你的代码,知道你的程序将在什么硬件上运行,然后利用这些优势,要知道的是现在几乎所有设备都是多核的,利用这些线程,不只是让你的程序顺序执行一条条指令,而是把一些东西推迟到不同的线程,甚至不是推迟,而是把东西分派到不同的线程,让计算机更快得处理这些东西
如何让 C++ 字符串更快
字符串会很慢,但这里不会谈论背后的技术细节,你只需要知道这是因为字符窜要分配内存,在堆上分配并不一定是坏事,事实上,在很多情况下,这是不可避免的,但如果可以避免,就尽可能的避免,因为它会降低程序的速度,std::string
和它的很多函数都喜欢分配,这实际上并不理想,在 Clion 编译器 C++ 17 中,std::string
做了SSO(Small String Optimization) ,对于长度大于 15 的字符串才会分配内存(我不确定其他编译器是否如此)
想要取消字符串的内存分配吗,这里有两种情况,一种是使用const char*
,另一种使用字符串视图,建议采取第二种,因为使用一个字符串可能是更加现实的情况,它可能来自一个文件或以某种方式生成的
std::string_view
是 C++ 17 中的一个新类,它的本质只是一个指向现有内存的指针,换句话说,就是一个const char *
,指向其他人拥有的现有字符串再加上一个大小 size,也可以理解成,创建了一个窗口,一个进入现有内存的小视图,而不是分配一个新的字符串。没有内存分配,按值传递字符串视图是非常轻量级的
基于 10000 次循环迭代后的基准测试,它会让你体会到内存分配对性能的影响有多大
可视化基准测试
小字符串优化
小字符串优化(SSO)通常是通过在对象内部预留一个足够大的缓冲区来实现的,在 64 位系统上,std::string
对象通常大小为 32 字节,这个大小包含了所有元数据和内部缓冲区
以下是std::string
对象内结构的典型示例:
- 元数据:包括长度、容量和其他必要的标志
- 内部缓冲区:用于存储小字符串字符数据
所以,一个典型的实现可能如下:
- 长度和容量:2 个
size_t
类型(在 64 为系统上每个占 8 字节),共 16 字节 - 内部缓冲区:16 个字节(15 个字符 + 1 个空终止符)
对于 15 个及以下字符的小字符串,字符数据可以直接存储在std::string
对象内部,超过 15 个字符的字符串将会触发内存分配,字符数据将存储在堆上,而对象内部只存储一个指向该数据的指针
跟踪内存分配的简单方法
基础代码实现如下:
1 | void *operator new(size_t size) { |