代码分支预测优化

代码分支预测优化

我们的代码中,if/else 代码编译后,一个分支的汇编代码紧随前面的代码,而另一个分支的汇编代码需要使用 JMP 指令跳转才能访问到。很明显,通过 JMP 访问需要更多的时间。

在复杂程序中,有很多的 if/else 语句,又或者是有频繁调用且有 if/else 语句的函数,每秒被调用几万次。通常程序员在分支预测方面做的很糟糕,编译器又不能精准的预测每一个分支,这是 JMP 指令产生的时间浪费就会很大。

一、likely 和 unlikely

Linux 内核代码中,在条件判断语句中有很多 likely()unlikely() 的调用。

1
2
#define likely(x)       __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

查看更多

可变参数

一、带有可变参数的宏

1. 方法一

在 1999 年版本的 ISO C 标准中,宏可以象函数一样,定义时可以带可变参数。宏的语法和函数的语法类似

1
#define debug(format, ...) fprintf(stderr, format, __VA_ARGS__)

这里的 ... 指可变参数。这类宏被表示成零个或多个符号,包括里面的逗号,一直到右括弧结束为止。

2. 方法二

GCC 可以给可变参数一个名字,如同其他参数一样。

查看更多

C++中字符串中删除空格

c++中的字符串中删除空格

默认情况下,下面的字符被视为空白字符:

1
2
3
4
5
6
1. 空格 ' '
2. 换行 '\n'
3. 回车 '\r'
4. 水平制表符 '\t'
5. 换页 '\f'
6. 垂直制表符 '\v'

std::remove_if 算法实际上并不从字符串中删除字符,而是移动所有“给定谓词”字符到前面,并返回一个指向结束位置的迭代器。然后我们可以通过调用 std::erase 来删除给定的谓词

查看更多

C++ 字符串拼接效率

C++字符串拼接效率

c++ 提供了 +=、append、stringstream、sprintf 这四种方式来拼接字符串。关于他们之间的性能做一个对比

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class StringEfficiency {
public:
StringEfficiency() {
genStrArr();
}

void plusTest() {
std::string res;
for (int i = 0;i < 10000; i++) {
res += vec[i];
}
}

void appendTest() {
std::string res;
for (int i = 0; i < 100000; i++) {
res.append(vec[i]);
}
}

void sprintfTest() {
char res[500000] = {0};
char* cp = res;
for (int i = 0; i < 100000; i += 3) {
sprintf(cp, "%s%s%s", vec[i].c_str(), vec[i+1].c_str(), vec[i+2].c_str());
cp += 15;
}
res[10] = 0;
}

void ssTest() {
std::stringstream ss;
for (int i = 0; i < 100000; i += 3) {
ss << vec[i];
ss << vec[i+1];
ss << vec[i+2];
}
}

private:
void genStrArr() {
srand((unsigned)time(nullptr));
for (int i = 0; i < 100000; i++) {
std::string str;
for (int j = 0; j < 5; j++) {
str.push_back('a' + rand() % 27);
}
vec.emplace_back(str);
}
}

private:
std::vector<std::string> vec;
};

int main() {
auto* se = new StringEfficiency();

// += 方式
struct timeval startTime{}, endTime{};
gettimeofday(&startTime, nullptr);
for (int i = 0; i < 1000; i++) {
se->plusTest();
}
gettimeofday(&endTime, nullptr);
long plusTime = (endTime.tv_sec - startTime.tv_sec)*1000000 + (endTime.tv_usec - startTime.tv_usec);
std::cout << "+= way cost time: " << plusTime << " us" << std::endl;

// append 方式
gettimeofday(&startTime, nullptr);
for (int i = 0; i < 1000; i++) {
se->appendTest();
}
gettimeofday(&endTime, nullptr);
long appendTime = (endTime.tv_sec - startTime.tv_sec)*1000000 + (endTime.tv_usec - startTime.tv_usec);
std::cout << "append way cost time: " << appendTime << " us" << std::endl;

// sprintf 方式
gettimeofday(&startTime, nullptr);
for (int i = 0; i < 1000; i++) {
se->sprintfTest();
}
gettimeofday(&endTime, nullptr);
long sprintfTime = (endTime.tv_sec - startTime.tv_sec)*1000000 + (endTime.tv_usec - startTime.tv_usec);
std::cout << "sprintf way cost time: " << sprintfTime << " us" << std::endl;

// stringstream 方式
gettimeofday(&startTime, nullptr);
for (int i = 0; i < 1000; i++) {
se->ssTest();
}
gettimeofday(&endTime, nullptr);
long ssTime = (endTime.tv_sec - startTime.tv_sec)*1000000 + (endTime.tv_usec - startTime.tv_usec);
std::cout << "stringstream way cost time: " << ssTime << " us" << std::endl;

delete se;
}

环境:

1
2
3
c++11
2.6 GHz 六核Intel Core i7
16G

查看更多

单例模式

一、单例模式实现

1. 线程不安全版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
public:
static Singleton* getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
private:
Singleton() = default; // 私有构造函数
public:
Singleton(const Singleton& other) = delete;
Singleton& operator=(const Singleton& other) = delete;
private:
static Singleton* m_instance;
};

这是最经典的实现方式,采用的是懒初始化的方式,但是在多线程的情况下,这种方式是不安全的。因为 m_instance = new Singleton() 不是一个原子操作,有可能在一个线程 new Singleton() ,还没有赋值给 m_instance 变量时,另外一个线程过来发现 m_instancenullptr ,又去初始化了。这就导致产生两个对象。

2. 加锁的版本

1
2
3
4
5
6
7
static Singleton* getInstance() {
std::lock_guard<std::mutex> lck(mtx);
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}

查看更多

C++ 中引用和指针的区别

c++ 中引用和指针的区别

  • 从概念上,指针是存放变量地址的一个变量,在逻辑上是独立的。他可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。而引用是一个别名,他的存在具有依赖性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的。
  • 指针传递参数和引用传递参数是有本质上的不同:
    1. 指针传递参数本质上是值传递,传递的是一个地址值。值传递的特点是被调函数对形参的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值
    2. 引用传递过程中,被调函数的形参虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。因此,被调函数对形参做的任何操作都影响了主调函数中的实参变量
查看更多

强制修改 const char 指针类型数据

强制修改const char*类型的数据

首先来说明const 的位置不同所带来的语法规则不同

1
2
const char* a = "hello";
char* const b = "hello";

const 放在 * 号的前面,代表指针指向的内容不变,const 放在 * 号的后面,代表指针的指向不能改变。且在编译时 “hello” 会放在常量区。常量区的变量只可读不可修改。

1
2
3
4
5
6
char* a = "hello";
const char* b = a;
a[1] = '1'; // ok
b[1] = '1'; // error
char* c = const_cast<char*>(b);
c[1] = '1'; // ok

查看更多

Linux 下 C/C++ 的时间编程

Linux 下 C/C++ 的时间编程

一、Linux 下时间类型

  • real time:日历时间。对于 Linux 中,这个时间的起点是 1970年1月1日00点,Linux 上以此为起点的均为 UTC 时间。

    格林威治时间(Greenwich Mean Time,GMT)也被称为世界标准的时间(Coordinated Universal Time,UTC)。UTC 和 GMT 两者是同一概念的时间。区别在于 UTC 是天文学上的概念,而 GMT 是基于一个原子钟。

    GMT 是中央时区,北京在东8区,相差8小时,所以 北京时间 = GMT时间 + 8小时

    注意:会受到修改系统时间的命令/api 或者 ntp 服务的影响,导致时间出现跳跃

查看更多

undefined

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
27
28
29
30
31
32
33
34
一. 关于栈上的内存分配
1. 栈上的内存分配极为简单,移动一下栈指针即可
2. 栈上的释放内存也极为简单,函数执行结束时移动一下栈指针即可
3. 由于后进先出的执行过程,不可能出现内存碎片
对于有构造和析构函数的非简单类型,c++编译器会在生成代码的合适位置,插入对构造和析构函数的调用。编译器会自动调用析构函数,包括在函数执行发生异常的情况。

二. RAII
c++支持将对象存储在栈上面,但某些情况对象不能存储在栈上。比如:
1. 对象很大
2. 对象的大小在编译时不能确定
3. 对象是函数的返回值,但由于特殊的原因,不应使用对象的值返回

不要返回本地变量的引用。在c++11之前,返回一个本地对象意味着这个对象会被拷贝,除非编译器发现可以做返回值优化(named return value optimization,或NRVO),能把对象直接构造到调用者的栈上。从c++11开始,返回值优化仍可以发生,但在没有返回值优化的情况下,编译器将试图把本地对象移动出去,而不是拷贝出去。这一行为不需要手工用 std::move() 干预,使用 std::move() 反而会影响返回值优化。

引用坍缩/引用折叠
在模版的使用中
template<typename T>
void f(T&& param)
f(10); // 10 是右值
int x = 10;
f(x); // x 是左值

这种未定的引用类型称为 universal references,这种类型必须被初始化,而他是左值还是右值则取决于他的初始化,如果被左值初始化,那它就是左值,反之亦然。
由于存在 T&& 这种未定的引用类型,当它作为参数时,又可能被一个左值引用或右值引用的参数初始化,这是经过类型推导的 T&& 类型,相比右值引用(&&)会发生类型的变化,这种变化就称为引用折叠
1. 所有右值引用折叠到右值引用上仍然是一个右值引用 (A&&&& 变成 A&&)
2. 所有的其他引用类型之间的折叠都将变成左值引用 (A&& 变成 A&; A&&& 变成 A&)
简单一点,如下:
1. 对于 template<typename T> foo(T&&) 这样的代码,如果传递过去的参数时左值,T的推导结果是左值引用;如果传递过去的参数是右值,T的推导结果是参数的类型本身
2. 如果 T 是左值引用,那 T&& 的结果仍然是左值引用---type&&& 坍缩为 type&
3. 如果 T 是一个实际类型,那 T&& 的结果自然就是一个右值引用

完美转发
保持参数的值类型:左值仍然是左值,右值仍然是右值。不改变原值的属性
实现原理:实现了两个模版函数,一个接收左值,另一个接收右值