IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 《C++性能优化指南》 linux版代码及原理解读 第七章 优化热点语句 -> 正文阅读

[系统运维]《C++性能优化指南》 linux版代码及原理解读 第七章 优化热点语句

?

语句优化一般来说是从执行流中移除指令的过程,这个是针对单一的语句的。

语句优化的问题在于,如果针对的语句不是函数调用而起其他的语句,这种语句的本身消耗的就很少。我们在进行语句优化的时候应该关注以下几种情况:

  • 循环
    循环所占用的开销是循环次数和循环内的开销的乘积。

  • 频繁调用的函数
    这种语句的开销是调用次数和函数内开销的乘积。

  • 贯穿这个程序的某些用例
    这个是针对全局代码而言的。如果我们在一个程序中一直使用std::string 但是做的只是简单的char操作的话,那如果我们把函数内的std::string 统一替换成char,也会有不少的提升。参考《C++性能优化指南》 linux版代码及原理解读 第四章

在嵌入式这种小型处理当中,对语句的优化效果可能会比较明显,但是在桌面级或者更高级的处理器上,有很多中硬件级别的优化,包括指令级别的并发和缓存等,这导致单一的一条语句造成的影响要小于内存操作造成的影响。

在进行语句优化的时候要注意编译器,不同的编译器、不同的版本、不同的优化方式都会造成不同的优化程度甚至反向优化。

.1 优化循环中的代码

原始代码如下 :

void testFunc_Origin(){
    char s[] = "This string has * many space (0x20) chars. *";
    for (size_t i = 0 ; i < sizeof (s); ++i){
        if (s[i] == ' ')
            s[i] = '*';
    }
}

for(part 1 ; part 2 ; part 3 ) 语句包含了三部分,第一部分是初始化语句,第二部分是从每次循环开始判断是否执行本次循环内的语句。第三部分是每次执行完成之后运行的。

从这里我们可以看到,part 2 是会一直运行的,如果在这部分当中包含了一个运行开销比较大的操作,那这部分就会造成很多的运行消耗。可以看到在debug模式下的运行输出

缓存条件语句

如果我们把part 2 当中的sizeof操作符号放到part 1 当中,代码如下:

void testFunc_RemoveSizeof(){
    char s[] = "This string has * many space (0x20) chars. *";
    for (size_t i = 0 ,len = sizeof (s); i < len; ++i){
        if (s[i] == ' ')
            s[i] = '*';
    }
}

可以看到在debug模式下的运行结果

testFunc_RemoveSizeof: start
testFunc_RemoveSizeof: stop Total take
407806 ticks and takes 407 mS

但是如果在release模式下,实际上这两种方式运行结果是相同的。

testFunc_Origin: start
testFunc_Origin: stop
Total take 11743 ticks and takes 11 mS
testFunc_RemoveSizeof: start
testFunc_RemoveSizeof: stop
Total take 11708 ticks and takes 11 mS
Process finished with exit code 0

这是因为编译器实际上为我们进行了优化。如果我们把编译器的优化等级调低的话,

set(CMAKE_CXX_FLAGS_RELEASE “-O2”)

那么结果就是

testFunc_Origin: start
testFunc_Origin: stop
Total take 18598 ticks and takes 18 mS
testFunc_RemoveSizeof: start
testFunc_RemoveSizeof: stop
Total take 34442 ticks and takes 34 mS

可以看到,在优化选项不同的时候,效果会不同。

更改循环方式

基本上说,for循环在编译后会编程 if / goto/类似的语句,这种跳转语句可能会降低运行速度,如果我们更换一种循环方式的话

void testFunc_DoWhile(){
    char s[] = "This string has * many space (0x20) chars. *";
    size_t i = 0, len = sizeof(s); // for循环初始化表达式
    do {
        if (s[i] == ' ')
            s[i] = ' ';
        ++i;                       // for循环继续表达式
    } while (i < len);             // for循环条件
}

结果如下:

testFunc_DoWhile: start
testFunc_DoWhile: stop
Total take 405896 ticks and takes 405 mS

移除不变性代码

像刚才我们把for循环的循环判断部分中的代码移除到初始化部分中,这种不会改变的代码(在这种情况下sizeof的结果是不会改变的)称为不变性代码,将这种代码移动到循环之外是一种常见的优化方式。

从循环中移除隐含的函数调用

C++代码可能会隐式地调用函数,而没有这种很明显的调用语句。以下是几种情况:
? 声明一个类实例(调用构造函数)
? 初始化一个类实例(调用构造函数)
? 赋值给一个类实例(调用赋值运算符)
? 涉及类实例的计算表达式(调用运算符成员函数)
? 退出作用域(调用在作用域中声明的类实例的析构函数)
? 函数参数(每个参数表达式都会被复制构造到它的形参中)
? 函数返回一个类的实例(调用复制构造函数,可能是两次)
? 向标准库容器中插入元素(元素会被移动构造或复制构造)
? 向矢量中插入元素(如果矢量重新分配了内存,那么所有的元素都需要被移动构造或是复制构造)

将循环放入函数以减少调用开销

如果程序需要访问一堆的数据,但是它在访问单个数据的时候需要调用函数进行处理当前的数据,那么在这个流程当中,会不停的触发函数调用的开销 。如果我们将循环中调用函数这种模式改成在调用函数当中运行循环,这样的一种方式称为循环倒置。

看如下代码:

void replace_nonprinting(char& c) {
    if (!isprint(c))
        c = '.';
}

void testFunc_ForLoopCall(std::string &str){
    for (unsigned i = 0, e = str.size(); i < e; ++i)
        replace_nonprinting(str[i]);
}

输出结果:

testFunc_ForLoopCall: start
testFunc_ForLoopCall: stop
Total take 2035282 ticks and takes 2035 mS

将函数replace_nonprinting的调用移除,改成如下代码:

void testFunc_revertForLoop(std::string &str){
    for (unsigned i = 0, e = str.size(); i < e; ++i){
        if (!isprint(str[i]))
            str[i] = '.';
    }
}

输出结果:

testFunc_revertForLoop: start
testFunc_revertForLoop: stop
Total take 1726703 ticks and takes 1726 mS

可以看到有一部分的优化效果。

优化频繁的检测

假设在一个event loop中,这个事件循环运行在主线程当中,他可以每秒执行1000个事物。如果我们要处理一个终止命令,这个判断需要多长时间判断一次呢?

首先是鼠标按键按下之后,如果我们立即检测事件队列,这时候我们一定会发现那个鼠标事件吗?答案是否定的。

当一个真实的机械按键被按下时会建立一个连接,而这个连接最初是断断续续的。在这种初始的断续状态下,一瞬间去查看这个连接的状态会误认为按键没有被按下。“消除抖动”使得按键被按下的消息被推迟至连接变为连续状态后才发送。50毫秒是一个常用的消除抖动间隔。

那如果像下面这样,我们在每次的事件循环当中都去调用poll_for_exit(),它会花费50ms来检测是否有退出键按下,如果按下就调用exit_program(),这将会花费400~600毫秒来停止程序。

void main_loop(Event &event){
    if( poll_for_exit() )    // process 50 ms
        exit_program();    // process 400 ms - 600ms
    else if ( event == mouse_event )
        ...
    else if ( event == warn_event )
        ...
    else ...
}

现在我们给程序加一个响应时间。假设我们认为用户对程序终止的响应时间在1s以内是完全可以接受的,程序的终止操作需要消耗400到600ms来处理,那现在我们应该如何处理这个终止事件呢?还需要每次都判断终止事件吗?

做一个简单地数学计算: 1s - 600 ms = 400 ms ;

只要我们在指令传过来之后的400ms之内开始执行exit_program()函数,这样就会在1秒钟之内完全结束掉程序。也就符合用户的预期。

那我们就需要将程序修改一下:

void main_loop(Event &event){
    static unsigned count = 1 ;
    if( count++ % 350 == 0 )
        exit_program();    // process 400 ms - 600ms
    else if ( event == mouse_event )
        ...
    else if ( event == warn_event )
        ...
    else ...
}

其他优化技巧

对于for循环的优化,网上有很多其他的优化技巧,比如展开循环、openmp或者其他的一些优化方式。感兴趣的可以上网上搜索一下。

.2 从函数中移除代码

函数调用

每次程序调用一个函数时,都会发生类似下面这样的处理(依赖于处理器体系结构和优化器设置)。

(1) 执行代码将一个栈帧推入到调用栈中来保存函数的参数和局部变量。

(2) 计算每个参数表达式并复制到栈帧中。

(3) 执行地址被复制到栈帧中并生成返回地址。

(4) 执行代码将执行地址更新为函数体的第一条语句(而不是函数调用后的下一条语句)。

(5) 执行函数体中的指令。

(6) 返回地址被从栈帧中复制到指令地址中,将控制权交给函数调用后的语句。

(7) 栈帧被从栈中弹出。

从流程上看,函数调用有一定的开销。当频繁的调用某个函数时,单纯调用函数的开销就会变大,参考之前循环倒置的部分。不过从某些情况下,函数调用也可能会有一些好处。比如和那些被内联展开(inline )的大型的函数来说,通常这种函数会更加紧凑一些。同时紧凑的函数有利于提高缓存和虚拟内存的性能。参考前面讲的内存页抖动、以及缓存名中率部分。
《C++性能优化指南》 linux版代码及原理解读 第二章

函数调用的基本开销

  • 函数参数

除了计算参数表达式的开销外,复制每个参数的值到栈中也会发生开销。

成员函数调用(与函数调用)

每个成员函数都有一个额外的隐藏参数:一个指向this类实例的指针,而成员函数正是通过它被调用的。这个指针必须被写入到调用栈上的内存中或是保存在寄存器中。

  • 调用和返回

函数调用和返回对程序功能的实现没有影响,同样我们可以将函数调用去掉,直接把函数体中的代码替换函数的调用来避免单纯的函数调用的开销。许多编译器对尝试使用这样的方式,即通过内联的方式进行优化。

  • 如果不能内联函数,那么函数的执行会产生开销。

当调用函数的时候,下一个执行地址EIP(指令指针)会被保存到栈中,函数返回时会将这个数据取出,然后从这个地址的指令继续向下运行。不过这几次都涉及到跨越非连续地址,可能会造成流水线停顿或者高速缓存未命中。

虚函数的开销

C++中,除了构造函数不能被定义为虚函数之外,其他的成员函数都可以被写成虚函数。带有虚函数的实例是通过添加一个指向虚函数表的指针来实现的,这个表中有虚函数的函数地址。虚函数表指针通常都是类实例的第一个字段,这样解引时的开销更小。

调用虚函数的时候有两次解引用操作。首先通过虚函数表的指针获得表的地址,其次通过虚函数表的初始地址加偏移量,解引用获得函数的真实执行地址。这两次操作都是非连续的内存操作,而每次这种非连续的内存操作都会增加流水线停顿和高速缓存未命中的概率。

虚函数的另一个问题是编译器难以内联它们。编译器只有在它能同时访问函数体和构造实例的代码(这样编译器才能决定调用虚函数的哪个函数体)时才能内联它们。

继承中的成员函数调用

  • 继承类中定义的虚成员函数

如果继承关系最顶端的基类没有虚成员函数,那么代码必须要给this类实例指针加上一个偏移量,来得到继承类的虚函数表,接着会遍历虚函数表来获取函数执行地址。这些代码会包含更多的指令字节,而且这些指令通常都比较慢,因为它们会进行额外的计算。这种开销在小型嵌入式处理器上非常显著,但是在桌面级处理器上,指令级别的并发掩盖了大部分这种额外的开销。

  • 多重继承的继承类中定义的成员函数调用

代码必须向this类实例指针中加上一个偏移量来组成指向多重继承类实例的指针。这种开销在小型嵌入式处理器上非常显著,但是在桌面级处理器上,指令级别的并发掩盖了大部分这种额外的开销。

  • 多重继承的继承类中定义的虚成员函数调用

对于继承类中的虚成员函数调用,如果继承关系最顶端的基类没有虚成员函数,那么代码必须要给this类实例指针加上一个偏移量来得到继承类的虚函数表,接着会遍历虚函数表来获取函数执行地址。代码还必须向this类实例指针加上潜在的不同的偏移量来组成继承类的类实例指针。这种开销在小型嵌入式处理器上非常显著,但是在桌面级处理器上,指令级别的并发掩盖了大部分这种额外的开销。

  • 虚多重继承

为了组成虚多重继承类的实例的指针,代码必须解引类实例中的表,来确定要得到指向虚多重继承类的实例的指针时需要加在类实例指针上的偏移量。如前所述,当被调用的函数是虚函数时,这里也会产生额外的间接开销。

后续会写博客专门 描述这几中情况的具体实现。

函数指针的开销

  • 函数指针(指向非成员函数和静态成员函数的指针)
    代码必须解引指针来获取函数的执行地址。编译器也不太可能会内联这些函数。
  • 成员函数指针
    成员函数指针声明同时指定了函数签名和解释函数调用的上下文中的类。

函数调用开销总结

C风格的不带参数的void函数的调用开销是最小的。如果能够内联它的话,就没有开销;即使不能内联,开销也仅仅是两次内存读取加上两次程序执行的非局部转移3。
如果基类没有虚函数,而虚函数在多重虚拟继承的继承类中,那么这是最坏的情况。不过幸运的是,这种情况非常罕见。在这种情况下,代码必须解引类实例中的函数表来确定加到类实例指针上的偏移量,构成虚拟多重继承函数的实例的指针,接着解引该实例来获取虚函数表,最后索引虚函数表得到函数执行地址。
此时,读者可能会惊讶函数调用的开销居然如此之大,抑或是惊叹C++居然如此高效地实现了这么复杂的特性。这两种看法都是合理的。需要理解的是正是有了函数调用开销,才有优化的机会。坏消息是除非函数会被频繁地调用,否则移除一处非连续内存读取并不足以改善性能;好消息则是分析器会直接指出调用最频繁的函数,让开发人员能够快速地集中精力于最佳优化对象。

善于使用内联函数

移除函数调用开销的一种有效方式就是内联函数。以下几种情况编译器通常会主动将函数进行内联:

在类的定义中同时定义的函数会被隐式内联。比如如下:

class inlineTest{
public:
    inlineTest() = default;
    void funtTest(){
        std::cout<<"Hello World"<<std::endl;
    }
    ...
}

通过将在类定义外部定义的函数声明为存储类内联,也可以明确地将它们声明为内联函数。

此外,如果函数定义出现在它们在某个编译单元中第一次被使用之前,那么编译器还可能会自己选择内联较短的函数。

在使用之前定义函数

??
当编译器编译对某个函数的调用时发现该函数已经被定义了,那么编译器能够自主选择内联这次函数调用。如果编译器能够同时找到函数体,以及实例化那些发生虚函数调用的类变量、指针或是引用的代码,那么这也同样适用于虚函数。

移除未使用的多态性

当基类中的某些虚函数在派生类中没有实现,或者永远不会实现的时候,这个时候移除virtual关键字可以提高调用的速度。

优化接口的实现方式。

一般来说我们在定义接口的时候,都是写一个由纯虚函数组成的抽象基类,然后将这个头文件提供给调用方,我们实现一个类通过继承这个抽象基类来实现具体的功能。比如:

// file.h——接口
class File { 
public:     
    virtual ~File() {}virtual bool Open(Path& p) = 0;     
    virtual bool Close() = 0;     
    virtual int GetChar() = 0;     
    virtual unsigned GetErrorCode() = 0; 
};

然后根据不同的平台,可以我们会写出如下的代码:

// Windowsfile.h——接口
# include "File.h" 
class WindowsFile : public File { // C++11风格的声明
public:     
    ~File() {} = default ;     
    bool Open(Path& p) override { ... };     
    bool Close() override { ... };     
    int GetChar() override { ... };     
    unsigned GetErrorCode() override { ... }; 
};
// Linuxfile.h——接口
# include "File.h" 
class LinuxFile : public File { // C++11风格的声明
public:     
    ~File() {} = default ;     
    bool Open(Path& p) override { ... };     
    bool Close() override { ... };     
    int GetChar() override { ... };     
    unsigned GetErrorCode() override { ... }; 
};

有时候,如果我们只实现了一种接口,比如只实现了windows的接口,那我们可以把其中的virtual 关键字去掉直接在file.h文件中实现。尤其是对于GetChar()这样的函数,每次获取一个字符都要调用一次虚函数的解析。

在链接时选择接口实现

如果无需在运行时做出选择的话,那么开发人员可以使用链接器来从多个实现中选择一种。具体做法是不声明C++接口,而是在头文件中直接声明(但不实现)成员函数,就像它们是标准库函数一样:

// file.h——接口
class File { 
public:     
    virtual ~File() {}virtual bool Open(Path& p) = 0;     
    virtual bool Close() = 0;     
    virtual int GetChar() = 0;     
    virtual unsigned GetErrorCode() = 0; 
};

在windowsfile.cpp文件中有如下Windows的实现代码:

// windowsfile.cpp——Windows的实现代码
# include "File.h" 
bool File::Open(Path& p) {    
    ... 
} 
bool File::Close() {    
    ... 
} 
    ...

同理在linux下实现linux.cpp,在其中将代码定义好。

这样做的方法,通过Cmake或者Qmake甚至Makefile等等项目管理的方式,我们可以指定程序具体链接哪个cpp文件,从而选择一种特定的实现方式。

在编译时选择接口实现

还有一种方式是将linux和windows的实现方式都在file.h中实现,但是通过宏来控制,类似下面的方式:

// file.cpp——实现
# include "File.h" 
# ifdef _WIN32     
bool File::Open(Path& p) {        
    ...     
}     
bool File::Close() {        
    ...     
}    
    ... 
# else // Linux     
bool File::Open(Path& p) {        
    ...     
}     
bool File::Close() {        
    ...     
}    
... 
# endif

用模板在编译时选择实现

模板的特性是一把双刃剑:一方面,即使开发人员在某个模板特化中忘记实现接口了,编译器也不会立即报出错误消息;但另一方面,开发人员也能够选择不去实现那些在上下文中没被用到的函数。从性能优化的角度看,多态类层次与模板实例之间的最重要的区别是,通常在编译时整个模板都是可用的。在大多数用例下,C++都会内联函数调用,用多种方法改善程序性能

避免使用PIMPL惯用法

PIMPL是“Pointer to IMPLementation”的缩写,它是一种用作编译防火墙——一种防止修改一个头文件会触发许多源文件被重编译的机制——的编程惯用法。

//代码清单 实现PIMPL惯用法之前的bigclass.h
# include "foo.h" 
# include "bar.h" 
# include "baz.h" 
class BigClass { 
public:     
    BigClass();     
    void f1(int a) { ... }     
    void f2(float f) { ... }     
    Foo foo_;     
    Bar bar_;     
    Baz baz_; 
};

要实现PIMPL,开发人员要定义一个新的类,在本例中,我们将其命名为Impl。bigclass.h的修改如代码所示。

代码清单 实现PIMPL惯用法之后的bigclass.h
class Impl; //前置声明
class BigClass { 
public: 
BigClass();    
    void f1(int a);     
    char f2(float f);     
    Impl* impl; 
};
代码清单 包含Impl的定义的bigclass.cpp
# include "foo.h" 
# include "bar.h" 
# include "baz.h" 
# include "bigclass.h" 
class Impl {     
    void g1(int a);     
    void g2(float f);     
    Foo foo_;     
    Bar bar_;     
    Baz baz_; 
}; 
void Impl::g1(int a) {    ... } 
char Impl::g2(float f) {    ... } 
void BigClass::BigClass() {     impl_ = new Impl; } 
void BigClass::f1(int a) {     impl_ -> g1(a); } 
char BigClass::f2(float f) {     return impl_ -> g2(f) }

实现了PIMPL后,在编译时,对foo.h、bar.h或baz.h,或者是对Impl的实现的改动都会导致bigclass.cpp被重编译,但是bigclass.h不会改变,这样就限制了重编译的范围。
在运行时情况就不同了。PIMPL给程序带来了延迟。之前BigClass中的成员函数可能会被内联,而现在则会发生一次成员函数调用。而且,现在每次成员函数调用都会调用Impl的成员函数。使用了PIMPL的工程往往会在很多地方使用它,导致形成了多层嵌套函数调用。更甚者,这些额外的函数调用层次使得调试变得更加困难。

移除对DDL的调用

一种改善函数调用性能的方式是不使用DLL,而 是使用对象代码库并将其链接到可执行程序上。

使用静态成员函数取代成员函数

实际上每次调用类的成员函数时,都会有一个隐式参数this指针被传递进去,而这个this指针就是这个类实例的地址。之所以有这么一个参数可以参考前面的虚函数部分,通过这个this指针加上偏移量,我们可以获得类成员的数据,解引this指针获取虚函数表指针。

从这里我们可以看到,如果一个成员函数不需要处理类内部的数据,也不需要其他的虚函数的话,这个this指针在这里没有什么意义。我们可以将这种函数声明为静态函数,这样就可以避免这种问题。因为同一个类的静态成员函数共享一个函数地址,所以没有this指针。

将虚析构函数移至基类中

任何有继承类的类的析构函数都应当被声明为虚函数。这是有必要的,这样delete表达式将会引用一个指向基类的指针,继承类和基类的析构函数都会被调用。

确保在这个基类中至少有一个成员函数,可以强制虚函数表指针出现在偏移量为0的位置上,这有助于产生更高效的代码。

.3 简化表达式

多项式 在C++中可以写为:

    y = a*x*x*x + b*x*x + c*x + d;

这条语句将会执行6次乘法运算和3次加法运算。

我们可以根据霍纳法则重复地使用分配律来重写这条语句:

    y = (((a*x + b)*x) + c)*x + d;

这条优化后的语句只会执行3次乘法运算和3次加法运算。

将常量组合在一起

seconds = 24 * 60 * 60 * days;
编译器会计算表达式中的常量部分,产生类似下面的表达式:
seconds = 86400 * days;

但是如果改成这样编译器就无法进行像刚才那样进行优化:

seconds = 24 * days * 60 * 60;

用更高效的运算符

有些数学运算符在计算时比其他运算符更低效。例如,整数表达式x*4可以被重编码为更高效的x<<2。

另一种优化是用位移运算和加法运算替代乘法。例如,整数表达式x9可以被重写为x8+x*1,进而可以重写为(x<<3)+x。

使用整数计算替代浮点型计算

在PC上面浮点型数据的计算开销是昂贵的,但是整型计算的速度比浮点计算快起码一个数量级 以上。

双精度类型可能会比浮点型更快

某些编译环境下,双精度类型的计算速度比浮点类型的计算速度更快。

用闭形式替代迭代计算

有时候我们会遇到一些问题使用时间复杂度O(n)的算法进行迭代求解,但是如果我们能转换思路,找到求解的更快的方法,这种方法的计算时间为常量,不进行任何迭代,这种方法称为闭形式。

具体的可以查找常量时间复杂度算法,后续我会写相关博客讨论相关算法。

.4优化控制流程惯用法

之前提到过,由于当指令指针必须被更新为非连续地址时在处理器中会发生流水线停顿,因此计算比控制流程更快。C++编译器会努力地减少指令指针更新的次数。了解这些知识有助于我们编写更快的代码。

用switch替代if-else

switch语句的参数实际上是一个常量,然后case中的很多个选项也是常量,这样switch语句会被编译程jump指令表,jump的索引值就是要测试的那个数值,case的数值也会被编译器进行排序,这样在理想情况下,case中的数值是连续的或者近似连续的,那么每次比较处理的开销都是O(1)。如果case中的选项跨度很大而且不连续,那么jump的指令表会很大,但是编译器可能会将其优化成适合二分查找的结构,这样最差的时间开销就是O()。

用虚函数替代switch或if

使用无开销的异常处理

如果有异常,处理错误的部分开销就被从程序执行的正常路径转移至错误路径上。除此之外,编译器会通过调用在抛出异常和try/catch代码块之间的执行路径上的所有自动变量的析构函数,自动地回收资源。这简化了程序执行的正常路径的逻辑,从而提升性能。
在C++的早期,每个栈帧都包含一个异常上下文:一个指向包含所有被构建的对象的链表的指针,因此当异常穿过栈帧被抛出时,这些对象也必须被销毁。随着程序的执行,这个上下文会被动态地更新。这并非大家所希望看到的,因为这导致了在程序执行的正常路径上增加了运行时开销。这可能会是高开销的异常处理之源。后来出现了一种新的实现方式,它的原理是将那些需要被销毁的对象映射到指令指针值上。除非抛出了异常,否则这种机制不会发生任何运行时开销。Visual Studio会在构建64位应用程序时使用这种无开销机制,而在构建32位应用程序时则会使用旧机制。Clang则提供了一个编译器选项6让开发人员选择使用哪种机制。

不要使用异常规范

异常规范有两个问题。一个问题是开发人员很难知道被调用的函数可能会抛出什么异常,特别是在使用不熟悉的库时。这使得使用了异常规范的程序变得脆弱且可能会突然停止。
第二个问题是异常规范对性能有负面影响。程序必须要检查被抛出的异常,就像是每次对带有异常规范的函数的调用都进入了一个try/catch代码块一样。
如果程序抛出列表之外的异常,那程序会被直接终止掉。

?

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-06-20 23:09:26  更:2022-06-20 23:10:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 1:30:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码