国内有时访问github会失败,这是为什么呢?

CDN(Content Delivery Network),即内容分发网络,也称为内容传送网络。它主要依靠部署在各地的边缘服务器,平衡中心服务器的负荷,就近提供用户所需内容,提高响应速度和命中率。我们访问的就是github的CDN。
DNS(Domain Name System), 即域名系统,它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。主要是做域名解析,域名最终指向的是IP地址。我们输入github.com的时候,会经过DNS解析,得到一个ip地址,电脑再使用这个ip地址来访问github。
DNS污染:就是域名系统被入侵或者认为的修改某些记录,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址。如果访问的DNS服务器被污染了,那么我们访问的网址ip可能就不对,导致访问失败。

解决方案:修改host文件

Windows:C:\Windows\System32\drivers\etc\hosts
Mac/Linux: /etc/hosts
优先级是host文件 > DNS服务器。host文件是本地文件,优先级最高,如果它中有指定的域名,那么就可以使用host文件中指定的IP地址;而DNS服务器则是远程服务,优先级次之,如果host文件中没有指定的域名,那么就会使用DNS服务器来查找相应的IP地址。我们的目的就是在hosts中告诉电脑,如果访问域名为github.com,那么他的ip就按xxx.xxx.xxx.xxx来算。

方法一

通过网站查询。https://tool.chinaz.com/dns, 输入github.com。得到ip地址,以

1
20.205.243.166  github.com

的方式加入到hosts文件末尾。

方法二

用ping命令查询得到其ip地址。

但是这个ip地址是会变化的,有没有更快捷的方法?

UsbEAm Hosts Editor

该工具可以快速测速,一键加入hosts地址,只要点点点的方式就可以执行上述组合操作。
下载博客地址:https://www.dogfight360.com/blog/475/
上个软件的截图
软件截图

C++多线程编程之std::future

1. std::future

std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值。如果共享状态的标志不为 还没有被 Provider 设置,则调用 std::future::get() 会阻塞当前的调用者,直到 Provider 设置了共享状态的值,std::future::get() 返回异步任务的值或异常(如果发生了异常)。一句话概括,std::future 是为了线程间传递数据使用的。
一共有三种 Provider:

  1. std::async
  2. std::promise 调用 get_future() 函数
  3. std::packaged_task 调用 get_future() 函数

提示,std::future 独占资源,类似 unique_ptr,不可被拷贝。接下来看下三种 Provider 的用法。

2. std::async

先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int main() {
std::future<int> fu = std::async([](int a, int b) {
std::cout << "thread start!" << std::endl;
return a + b;
}, 10, 20);

std::this_thread::sleep_for(std::chrono::milliseconds(2000));
int res = fu.get();
std::cout << "res=" << res << std::endl;
}

编译的时候需要链接pthread库。
运行结果:

1
2
thread start!
res=30

运行后立刻出现"thread start!"的字样,过了两秒出现结果。这是因为在运行 std::async 那一行的时候,直接会新建一个线程,执行函数的工作。这里使用的是 lambda 表达式,如果要传入自定义的函数需要加上 std::ref(函数名),传入函数引用(因为线程传入的是拷贝)。最后的 10 和 20 是运行所需的参数。之后主线程(主进程)睡眠 2000ms,阻塞在 fu.get() 上,直到线程运行完毕,最后打印结果。
综上所述,async 是为了把函数返回值带出来,用future来获取的。
这里注意有一个坑,如果不用 fu 来接收 std::async 的话,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int main() {
auto fu = std::async([](int a, int b) {
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
std::cout << "thread start!" << std::endl;
return a + b;
}, 10, 20);

std::cout << "main" << std::endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int main() {
std::async([](int a, int b) {
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
std::cout << "thread start!" << std::endl;
return a + b;
}, 10, 20);

std::cout << "main" << std::endl;
}

这两段代码运行结果有什么不同呢?
第一个用 fu 来接收了 std::future,导致这个 future 对象还没有被析构。所以是先打印 main,后面再过两秒打印出 thread start,之后 future 对象才因为程序结束离开作用域被析构。
第二个代码因为没有接收 future 对象,导致这个右值直接析构。所以主进程等待 future 析构完成再继续往下走,所以打印是先等待两秒,之后打印 thread start,紧接着打印 main。

3. std::promise

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int main() {
std::promise<int> pr;
auto fu = pr.get_future();
std::thread t1([](std::promise<int>& p, int a){
std::cout << "thread start!" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
p.set_value(a);
std::cout << a << std::endl;
}, std::ref(pr), 2);
std::cout << "main" << std::endl;
std::cout << "fu.get()=" << fu.get() << std::endl;
t1.join();
}

返回结果:

1
2
3
4
main
fu.get()=thread start!
2
2

首先promise返回了一个 future 对象,表示 promise 承诺要传一个数给 future。之后运行线程 t1,给他传入了 promise 和一个数。在线程中,给 promise 设置了一个值。此时外面的 future 就可以调用 get() 方法获得线程中设置的 a。至于打印为何会断开,是因为在调用 fu.get() 的时候阻塞在那里了,所以先把前半段打印出来,等待 promise 传值给 future。
综上所述,promise 是用来和 future 绑定,给他传数据的。

3. std::packaged_task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int main() {
std::packaged_task<int(int, int)> ta([](int a, int b) {
std::cout << "thread start!" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
return a + b;
});
auto fu = ta.get_future();
std::cout << "start to create thread" << std::endl;
std::thread t1(std::ref(ta), 2, 3);
std::cout << "fu.get()=" << fu.get() << std::endl;
t1.join();
}
1
2
3
start to create thread
fu.get()=thread start!
5

packaged_task 和一个普通的仿函数没有区别,只是返回值可以通过 future 来接收。结果同上,会在调用 fu.get() 的时候阻塞,等待线程 t1 执行完毕。
综上所述,packaged_task 是用来将一个 future 和一个函数绑定,返回函数执行结果的。和 async 的区别是它可以延迟启动,由 thread 来调用。当然你想要使用

1
2
ta(2, 3);
std::cout << fu.get();

那也可以,毕竟他也是个仿函数。

C++ 的 new

1
className a = new className;

new 是 C++ 的关键字,不能被重载。过程上分为两步:

  1. 分配内存。这一步底层就是调用了标题中提到的另一个 operator new 来完成的。也就是说最大的区别是 operator new 只负责分配内存,而 new 调用 operator new 并且调用构造函数。如果类 className 重载了 operator new(属于类里面的运算符),那么上述代码调用的就是
1
A::operator new(size_t)

来分配内存,否则调用的是

1
::operator new(size_t)

,由全局的 operator new 来分配。
2. 调用 className 的构造函数。

C++ 的operator new

相信看到这里已经知道他们的区别了,那么举个例子跑一下看看就清楚了!

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
#include <iostream>
using namespace std;
class A {
public:
static void * operator new(size_t size) { //重载类的operator new 改变new的行为
cout << "重载的类operator new 被调用!" << endl;
return malloc(size);
}
static void operator delete(void *ptr){ //重载类的operator delete 改变new的行为
cout << "重载的类operator delete 被调用!" << endl;
return free(ptr); //释放内存
}
static void * operator new[](size_t size) { //重载类的operator new[] 改变new[]的行为
cout << "重载的类operator new[] 被调用!" << endl;
return malloc(size);
}
static void operator delete[](void *ptr) { //重载类的operator delete[] 改变new[]的行为
cout << "重载的类operator delete[] 被调用!" << endl;
return free(ptr); //释放内存 }
}
};
int main()
{
A *a = new A();
A *b = new A[2];
delete (a);
delete[](b);
}

输出:

1
2
3
4
重载的类operator new 被调用!
重载的类operator new[] 被调用!
重载的类operator delete 被调用!
重载的类operator delete[] 被调用!

1. 单继承虚函数表

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
#include<iostream>
using namespace std;
class A {
private:
int a;
public:
virtual void f() {
cout<<"A::f()"<<endl;
}
virtual void g() {
cout<<"A::g()"<<endl;
}
};
class B:public A {
private:
int b;
public:
virtual void f() {
cout<<"B::f()"<<endl;
}
virtual void g1() {
cout<<"B::g1()"<<endl;
}
void h() {
cout<<"B::h()"<<endl;
}
};
int main()
{
typedef void(*fun)(void);
fun pFun;
A a;
B b;
return 0;
}

定义了两个对象,B继承自A。 B重写了A的f()函数,并新增了一个虚成员函数g1()和一个普通的成员函数h()。那么对象a,b的内存布局应该如下图所示:
1

口说无凭,我们用gdb打印一下看看。

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
$ gdb a.exe
GNU gdb (GDB) 7.6.1
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from F:\zkangHUST\C++\a.exe...done.
(gdb) start
Temporary breakpoint 1 at 0x40146e: file test3.cpp, line 32.
Starting program: F:\zkangHUST\C++/a.exe
[New Thread 10860.0x2e0c]
[New Thread 10860.0x3e64]
[New Thread 10860.0x3e94]
[New Thread 10860.0x8]

Temporary breakpoint 1, main () at test3.cpp:32
32 A a;
(gdb) n
33 B b;
(gdb)
51 return 0;
(gdb) p a
$1 = {_vptr.A = 0x405178 <vtable for A+8>, a = 4194432}
(gdb) p (int*)*((int*)0x405178)
$2 = (int *) 0x403c08 <A::f()>
(gdb) p (int*)*((int*)0x405178 + 1)
$3 = (int *) 0x403c3c <A::g()>
(gdb) p (int*)*((int*)0x405178 + 2)
$4 = (int *) 0x0
(gdb) p b
$5 = {<A> = {_vptr.A = 0x405188 <vtable for B+8>, a = 4200896}, b = 0}
(gdb) p (int*)*((int*)0x405188)
$6 = (int *) 0x403ca0 <B::f()>
(gdb) p (int*)*((int*)0x405188+1)
$7 = (int *) 0x403c3c <A::g()>
(gdb) p (int*)*((int*)0x405188+2)
$8 = (int *) 0x403cd4 <B::g1()>
(gdb) p (int*)*((int*)0x405188+3)
$9 = (int *) 0x3a434347
(gdb)

a的虚函数表地址是0x405178,把这个地址强制转换成int指针,对改指针取值即是虚函数表第一个函数的地址,可以转换成int指针,打印出来。可以看到,虚函数表跟我们分析的是一样的。这里有一个问题,可以看到A的虚函数表是以空地址结束的,B的虚函数结束的位置是一个随机值,可见虚函数表并不一定是以空地址结束。另外,B类新增的h()函数没有加入到虚函数表中,因为它不是一个虚函数,这个函数怎么调用已经在程序编译的过程中确定了(即所谓静态联编,也叫早期联编)。
同理,如果有第三个类C像下面这样继承类B。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C:public B {
private:
int c;
public:
virtual void f() {
cout<<"C::f()"<<endl;
}
virtual void g1() {
cout<<"C::g1()"<<endl;
}
virtual void k() {
cout<<"C::k()"<<endl;
}
};

那么C对象的内存应该如下图:
2
gdb打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) p c
$1 = {<B> = {<A> = {_vptr.A = 0x4051c0 <vtable for C+8>, a = 1948871853}, b = 4200912}, c = 6422368}
(gdb) p (int*)*((int*)0x4051c0)
$2 = (int *) 0x403d58 <C::f()>
(gdb) p (int*)*((int*)0x4051c0 + 1)
$3 = (int *) 0x403c4c <A::g()>
(gdb) p (int*)*((int*)0x4051c0 + 2)
$4 = (int *) 0x403dc0 <C::g1()>
(gdb) p (int*)*((int*)0x4051c0 + 3)
$5 = (int *) 0x403d8c <C::k()>
(gdb) p (int*)*((int*)0x4051c0 + 4)
$6 = (int *) 0x3a434347

2. 多继承(无虚函数覆盖)

单继承的虚函数表比较简单,现在来看下多继承的虚函数表是什么样的。首先看多继承无虚函数覆盖的情况。假设有四个类A,B,C,D。继承关系如下图。
3

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
class A {
private:
int a;
public:
virtual void f() {
cout<<"A::f()"<<endl;
}
virtual void g() {
cout<<"A::g()"<<endl;
}
};
class B {
private:
int a;
public:
virtual void f() {
cout<<"B::f()"<<endl;
}
virtual void g() {
cout<<"B::g()"<<endl;
}
};
class C {
private:
int a;
public:
virtual void f() {
cout<<"C::f()"<<endl;
}
virtual void g() {
cout<<"C::g1()"<<endl;
}
};
class D:public A,public B, public C {
private:
int a;
public:
virtual void h() {
cout<<"D::h()"<<endl;
}
};

子类继承了多个父类,在内存中会维持多张虚函数表,有几个父类就有几张虚函数表。同时,自己新加的虚函数会附加到第一个父类的虚函数表后面。类D的内存布局如图:
4

1
2
3
4
5
6
7
8
9
10
11
int main()
{
D d;
A* a = (A*)&d;
B* b = (B*)&d;
C* c = (C*)&d;
a->f();
b->f();
c->f();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(gdb) p d
$1 = {<A> = {_vptr.A = 0x4051f0 <vtable for D+8>, a = 6422368}, <B> = {_vptr.B = 0x405204 <vtable for D+28>, a = 4200896}, <C> = {
_vptr.C = 0x405214 <vtable for D+44>, a = 3981312}, a = 4194432}
(gdb) p (int*)*((int*)0x4051f0)
$2 = (int *) 0x403c08 <A::f()>
(gdb) p (int*)*((int*)0x4051f0 + 1)
$3 = (int *) 0x403c3c <A::g()>
(gdb) p (int*)*((int*)0x4051f0 + 2)
$4 = (int *) 0x403d88 <D::h()>
(gdb) p (int*)*((int*)0x4051f0 + 3)
$5 = (int *) 0xfffffff8
(gdb) p (int*)*((int*)0x405204)
$6 = (int *) 0x403c88 <B::f()>
(gdb) p (int*)*((int*)0x405204 + 1)
$7 = (int *) 0x403cbc <B::g()>
(gdb) p (int*)*((int*)0x405204 + 2)
$8 = (int *) 0xfffffff0
(gdb) p (int*)*((int*)0x405214)
$9 = (int *) 0x403d08 <C::f()>
(gdb) p (int*)*((int*)0x405214 + 1)
$10 = (int *) 0x403d3c <C::g()>
(gdb) p (int*)*((int*)0x405214 + 2)
$11 = (int *) 0x3a434347
(gdb)

可见结构跟我们能分析得到的虚函数表图是一致的。做类型强制转换之后,a指针指向D类中第一个虚函数表,b指针指向第二张虚函数表,c指针指向第三张虚函数表。
不过,从打印结果来看,a指向的地址与b指向的地址相差8,b指向的地址和c指向的地址也相差8。但是在32位系统中,一个指针所占用的字节数应该是4,为什么会是a,b,c之间会相差8呢?多出来的4字节其实是成员变量a所占用的字节。
我们的内存布局图应该是这样:
5
A类占用8字节,B类占用8字节,C类占用8字节,D类占用28字节((4+4)*3+4)以上就是多继承无虚函数覆盖的虚函数表和对象内存布局情况。下面看一下有虚函数覆盖的情况。

3. 多继承(有虚函数覆盖)

上例的继承关系保存不变,在D类中重写f()方法,修改类D的定义为:

1
2
3
4
5
6
7
8
9
10
11
class D:public A,public B, public C {
private:
int a;
public:
void f() {
cout<<"D::f()"<<endl;
}
virtual void h() {
cout<<"D::h()"<<endl;
}
};

6
也就是把A,B,C类虚函数表中各自的f()函数地址替换为D类重写的f()函数地址。

内存寻址定义

所谓内存寻址,就是 cpu 接受到指令后,需要从内存中取得相应数据。但是内存中的数据都是有对应地址的,如何通过地址拿到的地址,来获取相应地址段上的数据。

所谓地址在操作系统中分为逻辑地址,虚拟地址,线性地址和物理地址。

1. 逻辑地址

逻辑地址就是机器码指令用到的地址。机器指令码中用到的地址都是逻辑地址。目前这个地址是由 16 位段选择符和 32 位偏移量来表示的(CS:EIP 段选择符:段内偏移量)。

2. 虚拟地址

虚拟地址就是逻辑地址的段内偏移量。所以逻辑地址 = 段选择符:虚拟地址。我们正常代码中拿到的地址就是虚拟地址,比如:

1
int *p = (int*)malloc(sizeof(int));

3. 线性地址

是一个 32 位无符号整数,是由逻辑地址经过段页式转换而来的。我们常说的进程的地址空间,所谓的地址指的就是线性地址。

4. 物理地址

是内存芯片中的物理地址,是存放数据的实际地址。是由逻辑地址转换而来的。最终是由这个地址来定位到内存空间。在页表转换时,这里存的不是真正物理地址,是物理内存块编号。例如一个内存块大小为 4K,那第 0 块地址若为 0x50000000,第一块就为 0x50001000。

最后给一张地址变换的图:

0

x86 段页式内存管理机制

逻辑地址转换为物理地址需要经历两个过程:

1. 段式内存管理:逻辑地址 => 线性地址

逻辑地址是(selector:offset)的形式,selector 可以为代码段或者数据段。

1

如用 selector 去 GDT 全局描述符表(假定 TI = 0)拿到段基址 segment base address,之后再加上 offset,就得到了线性地址。这个过程,被称为段式内存管理。对于表指示器 Table Indicator 来讲,决定了去哪种表寻找描述符。全局描述符放在 GDT(每个 CPU 有一个)里面,进程自己的放在 LDT 里面。

分段的目的主要有两个:

  • 使操作系统可以访问大于地址总线的内存,如 32 位地址总线可以访问 大于 4G 的内存。
  • 权限控制,将每个段设置权限位,让不同程序可以访问不同段。

2. 页式内存管理:线性地址 => 物理地址

线性地址结构如下图:

2

线性地址切成三段,用前两段分别作为索引去 Page Directory 和 Page Table 里查表,会先得到一个页目录表项,再得到一个页表项(Page Table Entry),那里面的值就是一个物理内存块的起始地址(其实就是是物理内存编号),把它加上 线性地址 切分之后第三段的页内偏移就得到了最终的物理地址。我们把这个过程称作页式内存管理。

Linux 段页式管理做法

Linux 认为靠页式管理就能完成内核所需的功能了,段式太麻烦了。关又关不掉,因为是硬件那里做的,所以只能略施小计:所有段的 segment base address 都设置为 0!这样就不分段了,所有段内空间和线性空间重合。

逻辑地址 == 虚拟地址 == 线性地址

虚继承和虚基类

为了解决继承中的二义性问题,提出的虚基类和虚继承。虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的问题。

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
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
};

class B : public A {
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
};

class C : public A {
public:
C() {
cout << "C()" << endl;
}
~C() {
cout << "~C()" << endl;
}
};

class D : public B, public C {
public:
D() {
cout << "D()" << endl;
}
~D() {
cout << "~D()" << endl;
}
};
int main() {
D d;
// A* a = &d;
// test.cc: In function ‘int main()’:
// test.cc:46:13: error: ‘A’ is an ambiguous base of ‘D’
// A* a = &d;
// ^
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
@└────> # ./a.out 
A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()

可以看到,在以上存在多继承且为菱形继承的情况下,构造函数的调用顺序是 ABACD,这表明继承 B 和 C 的时候夹杂了两份 A 的内容。这样也有一个问题,就是在试图用基类指针指向 D 对象时,编译器不知道应该让它指向哪个 A 的内存,因为在 d 中有两个 A 的实例。虚继承就是为了解决这个问题的。

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
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
};

class B : virtual public A { // 虚继承,A 是 B 的虚基类
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
};

class C : virtual public A { // 虚继承,A 是 C 的虚基类
public:
C() {
cout << "C()" << endl;
}
~C() {
cout << "~C()" << endl;
}
};

class D : public B, public C {
public:
D() {
cout << "D()" << endl;
}
~D() {
cout << "~D()" << endl;
}
};
int main() {
D d;
A* a = &d;
return 0;
}
1
2
3
4
5
6
7
8
9
@└────> # ./a.out 
A()
B()
C()
D()
~D()
~C()
~B()
~A()

这样 D 的内存中就只有一份 A 了,并且用基类指针指向该地址也能正确进行。虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。

纯虚函数

cpp 支持编译时多态和运行时多态,函数重载就是编译时多态,而派生类和虚函数则为运行时多态。

编译时多态和运行时多态的区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是编译时多态,就是说地址是早绑定的。而如果函数的调用地址不能在编译期间确定,而需要在运行时才能决定,这这就属于运行时多态。 向上类型转换问题,对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。

Cpp 动态多态性是通过虚函数来实现的,虚函数允许派生类重新定义基类成员函数,而派生类重新定义基类虚函数的做法称为覆盖(override),或者称为重写。这部分原理可见另一篇文章。 cpp虚函数表

在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class)。

纯虚函数使用关键字 virtual,并在其后面加上 = 0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。virtual void fun() = 0,告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。

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
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "A()" << endl;
}
virtual void func() = 0; // 纯虚函数,表明 A 已经是抽象类了,不能实例化
virtual ~A() {
cout << "~A()" << endl;
}
};

class B : public A {
public:
B() {
cout << "B()" << endl;
}
void func() { // 实现该函数,否则 B 因为抽象类而不能实例化
cout << "B::func()" << endl;
}
~B() {
cout << "~B()" << endl;
}
};

int main() {
// A a;
//test.cc:26:7: error: cannot declare variable ‘a’ to be of abstract type ‘A’
// A a;
// ^
B b;
A* a = &b;
a->func();
return 0;
}
1
2
3
4
5
6
@└────> # ./a.out 
A()
B()
B::func()
~B()
~A()

std::atomic

原子类型是对数据的一种封装,可以防止数据竞争,达到同步多线程的内存访问的目的。对该变量的读写是原子的。

1
#include <atomic>

使用前需要包含头文件。该头文件中主要包含两个类:atomic 和 atomic_flag。本文主要讲解 std::atomic。

构造函数和赋值

1
2
3
4
5
6
7
8
atomic() noexcept = default;
constexpr atomic (T val) noexcept;
atomic (const atomic&) = delete;

T operator= (T val) noexcept;
T operator= (T val) volatile noexcept;
atomic& operator= (const atomic&) = delete;
atomic& operator= (const atomic&) volatile = delete;
  1. 构造一个未初始化的原子对象。
  2. 构造一个用 val 初始化的原子对象。
  3. 禁用拷贝构造函数,原子对象不可复制、移动。
  4. 可以赋值(val)。

memory_order

1
2
3
4
5
6
7
8
typedef enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
} memory_order;

它们的目的是为了做线程间的同步,原理是在线程内限制变量操作的顺序:

  1. memory_order_relaxed:用于读写,不做任何限制。
  2. memory_order_acquire:用于读,如果一个原子变量的 load 用了该选项,那么可以保证,在本线程内,该 load 语句之后的所有变量(不论是否原子变量)的读写语句,都实际在该 load 操作执行后执行。
  3. memory_order_release:用于写,如果一个原子变量的 store 用了该选项,那么可以保证,在本线程内,该 store 语句之前的所有变量(不论是否原子变量)的读写语句,都实际在该 store 操作执行前执行。
  4. memory_order_consume:用于读,如果一个原子变量的 load 用了该选项,那么可以保证,在本线程内,该 load 语句之后的所有依赖该变量的变量的读写语句,都实际在该 load 操作执行后执行。但是,它只保证与当前操作相关的数据依赖关系。从 2016 年后,所有编译器实现中,memory_order_consume 和 memory_order_acquire 完全一致。
  5. memory_order_acq_rel:memory_order_acquire + memory_order_release
  6. memory_order_seq_cst:sequence consistent,顺序一致,要求所有变量的读写执行顺序都和代码中的顺序一致。

memory_order_acquire 和 memory_order_release 直观上就像一个栅栏:在调用处设置一个栅栏,acquire 是拦住代码中在它后边的变量读写操作,不让其跑到它前边;release则相反,不让前边的跑到后边。(cpu 提供 memory_barrier 或者 memory_fence 指令)关于为什么 cpu 会进行内存重排,请见内存模型与内存序。

atomic::store()

1
2
void store (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
void store (T val, memory_order sync = memory_order_seq_cst) noexcept;

用 val 替换原子对象中的值。该操作是原子性的,通过 sync 指定内存顺序。sync 可选项见上文。

atomic::load()

1
2
T load (memory_order sync = memory_order_seq_cst) const volatile noexcept;
T load (memory_order sync = memory_order_seq_cst) const noexcept;

返回原子对象中的值。该操作为原子性。

atomic::exchange

1
2
T exchange (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T exchange (T val, memory_order sync = memory_order_seq_cst) noexcept;

用 val 替换原子对象中的值,并返回替换前的值。操作为原子性。整个过程完成之前,其他线程无法访问。

atomic::compare_exchange_weak

1
2
3
4
5
bool compare_exchange_weak (T &expected, T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak (T &expected, T val, memory_order sync = memory_order_seq_cst) noexcept;

bool compare_exchange_weak (T &expected, T val, memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_weak (T &expected, T val, memory_order success, memory_order failure) noexcept;

将原子对象的存储值和预期值比较:

  • 若为 true,用 val 替换原子对象值。
  • 若为 false, 用包含的值替换预期值。

整个过程是原子性的。

下面版本使用的内存顺序取决于比较结果:true 则使用 success; false 则使用 failure。该函数比较的是原子对象和预期值中的物理内容,这可能导致使用操作符 == 比较相等的值的在这里比较失败。

与 compare_exchange_strong 不同,该 weak 版本允许错误的返回 false,即使原子对象存储值与预期值相等。对于某些循环算法,这可能是可接受的行为,并且可能在某些平台上显著提高性能。对于这些虚假的失败,函数返回 false,但不修改预期的值。对于非循环算法,通常首选 compare_exchange_strong。

atomic::compare_exchange_strong

1
2
3
4
5
bool compare_exchange_strong (T &expected, T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_strong (T &expected, T val, memory_order sync = memory_order_seq_cst) noexcept;

bool compare_exchange_strong (T &expected, T val, memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_strong (T &expected, T val, memory_order success, memory_order failure) noexcept;

原子操作。与 compare_exchange_week 不同,当期望值与对象存储的值相等时,这个强版本必须始终返回 true,不允许虚假的失败。但是,在某些机器上,对于某些在循环中检查这个的算法,compare_exchange_weak 可能有更高的性能表现。

专门化计算操作

atomic::fetch_add(T val, memory_order sync = memory_order_seq_cst)

存储的值 + val 并返回之前的值,整个操作是原子性的。如果第二个参数使用默认值,那么这个函数相当于 atomic::operator+=。

atomic::fetch_sub(T val, memory_order sync = memory_order_seq_cst)

存储的值 - val 并返回之前的值,整个操作是原子性的。如果第二个参数使用默认值,那么这个函数相当于 atomic::operator-=。

atomic::fetch_and(T val, memory_order sync = memory_order_seq_cst)

(存储的值 & val)并返回之前的值,整个操作是原子性的。如果第二个参数使用默认值,那么这个函数相当于 atomic::operator&=。

atomic::fetch_or(T val, memory_order sync = memory_order_seq_cst)

(存储的值 | val)并返回之前的值,整个操作是原子性的。如果第二个参数使用默认值,那么这个函数相当于 atomic::operator|=。

atomic::fetch_xor(T val, memory_order sync = memory_order_seq_cst)

(存储的值 ^ val)并返回之前的值,整个操作是原子性的。如果第二个参数使用默认值,那么这个函数相当于 atomic::operator^=。

atomic::operator++ ()

递增保存的值,返回递增后的值。

atomic::operator++ (int)

递增保存的值,返回递增前的值。

atomic::operator-- ()

递减保存的值,返回递减后的值。

atomic::operator-- (int)

递减保存的值,返回递减前的值。

reference:https://blog.csdn.net/u014673282/article/details/89789139

什么是内存泄漏?

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或
无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

valgrid工具

(1) 简介:
Valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合。Valgrind 由
内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模
拟了一个 CPU 环境,并提供服务给其他工具;而其他工具则类似于插件 (plug-in),利用内
核提供的服务完成各种特定的内存调试任务。最常用的工具为 Memcheck,用来检测程序中
出现的内存问题,所有对内存的读写都会被检测到,一切对 malloc()/free()/new/delete 的调用
都会被捕获。所以,它能检测以下问题:
• 对未初始化内存的使用;
• 读/写释放后的内存块;
• 读/写超出 malloc 分配的内存块;
• 读/写不适当的栈中内存块;
• 内存泄漏,指向一块内存的指针永远丢失;
• 不正确的 malloc/free 或 new/delete 匹配;
• memcpy()相关函数中的 dst 和 src 指针重叠。
(2) 使用示例:
valgrind --log-file=report.log --tool=memcheck --leak-check=full --show-leak-kinds=all ./benc
hmark arg1 arg2 …
(3) 输出日志解读:
• 报告中会指示出发生泄漏的函数和文件位置;
• 最后会给出一个总结,标明了泄漏的内存总量;
• definitely lost: 铁定丢失,一般来说存在明显的泄漏;
• indirectly lost: 间接丢失,当使用了含有指针成员的类或结构时可能会报这个错误;
• possibly lost: 可能丢失;
• 只能定位到内存最初被申请的位置,不能定位到是哪里泄漏

AddressSantizer

(1) 简介:
AddressSanitizer(ASan)是一个快速的内存错误检测工具。它包括一个编译器 instrumentation 模块和一个提供 malloc()/free()替代项的运行时库。
(2) 使用:
编译和链接选项中加入-fsanitize=address -fno-omit-frame-pointer 即可。可以通过 readelf 工具查看.so 的依赖库中是否包含 ASan 来确认编译是否成功。

推理性能优化

推理优化工作可以归成四类

  1. 算子优化
  2. 图优化
  3. 模型压缩
  4. 部署优化

算子优化

算子优化就是优化单算子的性能,方法无非是算法优化和微架构优化。

对同一个算子可能有不同的算法去实现它。举个例子,对卷积算法我们常见的就有:矩阵乘法,直接卷积法,Winograd 变换法,FFT 变换法。需要我们针对目标问题,分析并实现合适的算法,来达到最佳性能。
微架构优化。微架构优化主要焦点是如何充分利用好微架构的内置加速器的能力去最大化算子的性能。

图优化

图优化主要通过子图变换和算子融合的方式来达到减少计算量或者其他系统开销(如访存开销),从而达到性能优化的目的。图优化主要是希望在不影响模型的数值特性的基础上,通过图变换达到简化计算、资源开销,提升性能,所以是性能优化时的首选方法之一。

子图变换

子图变换主要是通过数学变换的手段对图进行精简,从而减少计算和调度开销。常见的有常数折叠,公共子表达式折叠以及算术变换。

常数折叠 (Constant Folding)

1

2

算数变换

3

算子融合

在深度学习中,一般来说,计算密集型和访存密集型算子是相伴出现的。这时候我们可以通过 fusion 来实现寄存器计算,从而减少访存密集型算子的访存,减少内存访问延时和带宽压力,提高推理效率。

4

5
通过这种方式,减少两次 tensor 的内存读写操作。

模型压缩

上面的方案都是精度无损的,当这三点都做完了后,如果还需要额外的性能增益,这时候需要考虑模型压缩方案。

模型量化

模型量化主要是通过降低模型中 tensor 和 weights 精度的手段,从而减少计算需求和数据存储与传输需求,来达到加速的目的。例如把权重从 FP32 降低为 FP16。虽然可以加速,但是有一定的精度损失。

模型蒸馏

模型蒸馏采用的是迁移学习的方法,通过采用预先训练好的复杂模型(Teacher Model)的输出作为监督数据,去训练另外一个简单的网络(Student Model),最后把 Student Model 用于推理。有精度损失。

部署优化

部署优化主要通过调整模型在部署时的资源分配和调度的参数来进一步优化性能。如,调整了 NUMA 的参数。

cpp 的构造函数种类

  1. 构造函数:生成一个新的对象。
  2. 拷贝构造函数:参数是 const T& x,用于拷贝。
  3. 赋值构造函数:这个严格意义来说不是构造函数,是赋值运算符 = 的重载。初始化的时候不会调用这个函数!
  4. 移动构造函数:参数是 T &&x,用于移动构造,提升效率。

代码示例

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
#include <iostream>
#include <string>

using namespace std;

class Integer {
private:
int* m_ptr;
public:
// 普通构造函数
Integer(int value) : m_ptr(new int(value)) {
cout << "constructor" << endl;
}
// 拷贝构造函数
Integer(const Integer& source) : m_ptr(new int(*source.m_ptr)) {
cout << "copy constructor" << endl;
}
// 移动构造函数
Integer(Integer&& source) : m_ptr(source.m_ptr) {
source.m_ptr = nullptr;
cout << "move constructor" << endl;
}
// 赋值运算符重载
Integer& operator=(const Integer& source) {
m_ptr = new int(*source.m_ptr);
cout << "copy assignment constructor" << endl;
return *this;
}
~Integer() {
cout << "destructor:" << GetValue() << endl;
delete m_ptr;
}
int GetValue(void) {
if (m_ptr) {
return *m_ptr;
} else {
return -1;
}
}
};

Integer getNum() {
Integer a(100);
return a;
}
int main(int argc, char const* argv[]) {
Integer a(getNum());
cout << "a=" << a.GetValue() << endl;
cout << "-----------------" << endl;
Integer temp(10000);
Integer b(temp);
cout << "b=" << b.GetValue() << endl;
cout << "-----------------" << endl;
Integer c = b;
c = a;
cout << "c=" << c.GetValue() << endl;
cout << "-----------------" << endl;
return 0;
}

在编译的时候一定要指定编译选项 --no-elide-constructors,否则会被编译器优化!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@└────> # ./a.out 
constructor
move constructor
destructor:-1
move constructor
destructor:-1
a=100
-----------------
constructor
copy constructor
b=10000
-----------------
copy constructor
copy assignment constructor
c=100
-----------------
destructor:100
destructor:10000
destructor:10000
destructor:100

分析

a

  1. 第一个 constructor,是在 getNum() 中首句 Integer a(100) 所打印的,调用默认的构造函数。
  2. 第二个 move constructor 是因为 return a 的时候,这个 a 是个右值,作为 main 函数 getNum() 的返回值,而调用的移动构造函数,将 getNum() 函数内的返回值移动构造给 main 函数接收 getNum() 的对象。
  3. 第三个 destructor 是析构 getNum() 中的对象 a。
  4. 第四个 move constructor 是因为 Integer a(右值),所以调用移动构造函数。
  5. 第五个 destructor 是析构 Integer a(右值) 中的右值而显示的。

b

  1. 第一个 constructor 是 Integer temp(10000); 调用的默认构造函数。
  2. 第二个 copy constructor 是 Integer b(temp); 调用的拷贝构造函数,将 temp 拷贝给 b;

c

  1. 第一个 copy constructor 是 Integer c = b; 调用的拷贝构造函数,可以看到此处并没有调用拷贝赋值运算符,因为这是在初始化!
  2. 第二个 copy assignment constructor 是 c = a 调用的才是拷贝赋值运算符。

可以看到,初始化的时候不会调用 拷贝赋值运算符!!!

析构

最后的四个 destructor 是析构。根据规则,先构造的后析构,后调用的先析构。析构顺序:c = 100,b = 10000,temp = 10000,a = 100。