C++ 技巧篇

 
 
C++
 4.1k
 

在观看了 Cherno 的 C++ 系列视频后的一些记录——技巧篇,囊括了有关 C++ 的性能操作、功能性操作

2024-07-27 11:06:29

目录:

std::vector的优化使用

创建与使用库

如何处理多返回值

为什么不使用using namespace std

线程

计时

条件与操作断点

预编译头文件

基准测试

可视化基准测试

小字符串优化

跟踪内存分配的简单方法


std::vector的优化使用

我们创建类对象时,实际上是在主函数的当前栈帧中构造它,因此需要把类对象从mian函数中复制到vector类中。这是可以优化的第一件事,在vector分配的内存中构造类对象

1
2
3
4
5
6
std::vector<Vertex> vertices;

// 只传递了构造函数的参数列表,在实际的`vector`内存中使用以下参数构造`Vertex`对象
vertices.emplace_back(1, 2, 3);
vertices.emplace_back(4, 5, 6);
vertices.emplace_back(7, 8, 9);

提前告知需要的内存大小(2个对象),这样就不必调整大小了,这就是第二种优化策略

1
2
3
std::vector<Vertex> vertices;

vertices.reserve(2);

创建与使用库

如何处理多返回值

在代码中可以看的比较清晰

堆与栈内存的比较

当我们在栈中分配变量时,发生的是栈指针基本上移动变量大小的字节,如果我们想分配一个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Timer {
public:
Timer() {
m_start_time_point_ = std::chrono::high_resolution_clock::now();
}

~Timer() {
Stop();
}

void Stop() {
auto end_time_point = std::chrono::high_resolution_clock::now();

auto start_time =
std::chrono::time_point_cast<std::chrono::microseconds>(m_start_time_point_).time_since_epoch().count();
auto end_time = std::chrono::time_point_cast<std::chrono::microseconds>(end_time_point).time_since_epoch().count();

auto duration = end_time - start_time;
auto ms = duration * 0.001;

std::cout << duration << "us( " << ms << " ms)\n";

}
private:
std::chrono::time_point<std::chrono::high_resolution_clock> m_start_time_point_;
};

类型双关

我们可以用不同的方式(转换成指针)解析一段内存,从而得到不同的结果,类型只是约定的解析内存的方式

条件与操作断点

预编译头文件

基准测试

当你正在处理一个对性能相当关键的部分,或者你正在测试你刚刚学过的新技术,你想把它的性能和过去使用的方法做个比较,看看那个性能好。有些人喜欢依赖第三方工具,有些人喜欢对代码进行仪表化处理,给它们设置计时器之类的东西,有些人喜欢运行他们的程序,把程序封装在一个计时器中,然后测试特定的程序。

值得注意的是,无论你在测试什么,都需要确保你确实做了这些事情,因为编译器实际上会非常积极地改变你的代码,具体可见

如何让 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
2
3
4
5
6
7
8
9
void *operator new(size_t size) {
std::cout << "Allocating " << size << " bytes\n";

return malloc(size);
}

void operator delete(void *memory) {
free(memory);
}

可以看一下demo

Comments