美文网首页
C++(98/11)问题 (1)

C++(98/11)问题 (1)

作者: 一叶也知秋 | 来源:发表于2021-02-06 14:20 被阅读0次

    一、内存问题

    1、堆和栈

    首先需要确定的是这里堆和栈不是数据结构中堆和栈的概念。

    • 相同点:
      都分别叫heap stack 翻译完全一样。
    • 不同点
      数据结构中的堆,大部分在堆排序这一部分的内容中,堆指的是数据放在一起的直观概念。 栈的数据结构主要就是一个先入后出的概念,在解决很多问题时候的基础数据结构定义。
      但是在OS内存的堆存储区和栈存储区的概念则有不同。
      操作系统启动后,给每一个启动后的应用分配一个虚拟存储区,由系统将虚拟存储区的内存地址映射到物理地址(具体映射方式和内存管理方式详细参见操作系统的基础原理)。一个应用程序的操作系统内存分配分为以下几个部分:
      1、栈区(stack) — 由编译环境决定 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。其内存的分配由系统决定。
      2、堆区(heap) — 一般由应用程序的编写者来管理分配和释放, 若应用本身不释放,这中间可能会造成内存泄漏等,程序结束时可能由OS回收 。
      3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 应用程序结束后OS释放。
      4、字符串常量区 — 常量字符串就是放在这里的。 应用程序结束后OS释放。
      5、程序代码区 — 存放函数体的二进制代码。
      (如果要应对这其中的问题,对于const 控制的参数,形参等,具体参见后文的const,static用法等)
      根据以上概念,栈是由系统分配好的一块连续内存存储区域,(Windows上一般为2M大小,Linux上由于芯片架构的差异,略有不同 具体可以用ulimit -a查询所有的内存,其中ulimit -s可以查到栈内存区的大小);而堆的话一般就是通过malloc(C语言),new(C++语言)来调用的,而malloc分配内存需要一整套比较复杂点的算法,而在栈上分配内存就简单方便的的多,直接在已经固定的内存区域取用即可,其分配管理方式就是基于栈的管理方式,并且还有专门的寄存器来管理栈顶位置,这个在效率上要比在堆上效率要好的多。其中malloc在堆内存区分配的时候的算法,以及可能带来的问题参见下文)。

    2、new/delete,malloc/free 的问题说明

    • 重点说说new 和 malloc的事情
      首先所有的内存分配都可以用malloc来分配,new是专属C++,具体怎么实现,可以参考重载后的代码实现部分,可以选择是否使用malloc来实现,new可以有返回类型,由编译器来计算分配内存的大小,可以用new[] 来分配分好组的内存。可以被重载实现,异常(比如分配不到内存等情况)有明确的返回。 malloc就是一件事:根据传入的参数从堆内存区中分配好内存并返回分配好后的指针,如果没成功则返回NULL。
    • malloc内存分配算法:
      todo

    3、 内存泄漏以及智能指针的问题

    内存泄漏就是已经分配的内存,不再有被使用的可能直到该程序结束后被OS回收。
    在C++中,一般就是由于各种复杂逻辑导致的new/delete, malloc/free没有成对出现或者有些情况下,程序异常退出某个循环结构后没有正常free or delete。
    内存泄漏问题的解决:

    内存泄漏检查工具

    智能指针

    早期C++开始,用
    auto_ptr(new int)
    来定定义指针,这样就不需要手动写删除的代码了。
    到C++11开始后,C++11标准中改用unique_ptr、shared_ptr及weak_ptr等智能指针来自动回收堆分配的对象。这里我们可以看一个C++11中使用新的智能指针的简单例子。

    unique_ptr<int>up1(new int(11));          //无法复制的unique_ptr
    unique_ptr<int> up2=up1;                  //不能通过编译
    cout<<*up1<<endl;                        //11
    unique_ptr<int>up3=move(up1);            //现在p3是数据唯一的unique_ptr智能指针
    cout<<*up3<<endl;                          //11
    cout<<*up1<<endl;                         //运行时错误
    up3.reset();                              //显式释放内存
    up1.reset();                              //不会导致运行时错误
    cout<<*up3<<endl;                        //运行时错误
    shared_ptr<int>sp1(new int(22));
    shared_ptr<int>sp2=sp1;
    cout<<*sp1<<endl;                        //22
    cout<<*sp2<<endl;                        //22
    sp1.reset();
    cout<<*sp2<<endl;                       //22
    

    unique_ptr, shared_ptr, weak_ptr, make_unique
    当智能指针遇到右值引用??
    可以先看现代C++部分后再返回此处。????

    关于指针和引用 std::function std::bind //todo

    二、面向对象的问题

    1、内存模型

    C++编译器自动生成的函数

    在C++98编译器中

     class A {} 
    

    编译器给生成的代码如下:

    class A  {
    public:
      A();                       // 构造函数
      A(const A& a);             // 拷贝构造函数
      virtual  ~A();              // 析构函数,此处应该是virtual
      A& operator=(const A& a);    // 拷贝赋值函数
    }
    

    C++11编译器之后, 首先有了右值引用的加入,默认生成的函数增加了 移动构造函数和移动拷贝函数 ,此时代码生成如下:

    class A  {
    public:
      A();                       // 构造函数
      A(const A& a);             // 拷贝构造函数
      A(A&& a);             // 移动构造函数,此处右值引用
      virtual  ~A();              // 析构函数,此处应该是virtual
      A& operator=(const A& a);    // 拷贝赋值函数
      A& operator=( A&& a);    // 移动赋值函数
    }
    

    = delete 和 = default 两个工具

    • = default
      如上所述,如果默认的构造函数A() 其中有针对其中成员变量进行初始化默认值的话,就一定需要自己写下这部分代码,而不能依赖编译器自动生成;但是编译器自动生成的函数比手动编写的执行效率要高,在此之前,似乎没有找到可以更高效执行自己手写函数的办法,在C++11之后,加上 = default的可以让手写的这部分代码获得同等于自动生成代码的执行效率。

    • = delete
      使得编译器在生成这段代码的时候,如果有定义 = delete 就默认禁用该函数。
      用处,例如

    int add(int m, int n) = delete;
    float add(float m, float n);
    

    此时int类型就不能作用add函数的输入参数。

    手动实现 class String的例子,例子中对于构造函数,拷贝构造函数,移动构造函数(C++11新增)赋值函数,析构函数(本例子中没考虑base类的情况,如果是base类的情况,需要是virtual函数),重要相关的均已注释。

    class String  {
    public:
      // 构造函数
    String(char *str = NULL){
         //空构造也会创建一个长度为1的字符串数组
        if(str == NULL){
            m_data = new char[1];
            m_data[0] = '\0';
            m_size = 0;
        }
        else{
            m_size = strlen(str);
            m_data = new char[m_size + 1];
            strcpy(m_data, str);
        }
    }
    
    //拷贝构造函数
    String(const String &str){
        m_size = str.m_size;
        m_data = new char[m_size + 1];
        strcpy(m_data, str.m_data);
    }
    
    //移动构造函数, 此处右值引用
    String(String&&  str) noexcept  // noexcept 用处?
         :m_size(str.m_size) {  
        cout<<"construct &&"<<endl; 
        str.m_size = 0;
        m_data = str.m_data;
        str.m_data = nullptr;   // 移动之后原来的引用就废止了
    };
    
     ~String()  {
        delete [] m_data;
      } ;   
           
      String& operator=(const String& a)  {
        if(this == &str) { 
             return *this;
        }
    
        delete [] m_data;
        m_size = str.m_size;
        m_data = new char[m_size + 1];
        strcpy(m_data, str.m_data);
        return *this;
    }
    
        String& operator=( String&& str)  noexcept  {    // 移动赋值函数
           if(this == &str) {
              return *this;
           }
        
           delete [] m_data;
           m_size = str.m_size;
           m_data = str.m_data;
           str.m_data = nullptr;
           return *this;
      } 
    
    private:
      char * m_data;
      int m_size;
    }
    

    其中的移动构造函数
    也可以写成这样

     // Move constructor.
    String(String&& other) noexcept
       : m_data(nullptr)
       , m_size(0) {
        cout<<"construct &&  std::move"<<endl; 
           *this = std::move(other);
        }
    

    虚函数的问题

    面向对象虚函数 菱形继承 todo

    菱形继承问题

    面向对象虚函数 菱形继承 todo

    2、面向对象的基础概念

    继承 组合问题 设计模式相关问题 todo

    三、const 、static

    const的作用:

    • 定义常量
    const int constValue = 10;    // 一般为定义一个常量
    
    • 修饰变量
    const int* p = 10;   // 表示指针p指向的内容10 是常量,不可变
    char * const p;      // 就是将p声明为常指针,它的地址不能改变
    const double *const p // 这里指的是指针是常量,指针指向的内容也是常量
    
    • 修饰函数的形参
    void fun(const char* ch);  // 参数指针所指的内容为常量不可变
    void fun(const int v);  // 表示传入的参数不可变,但是由于是形参,此处const无意义
    void fun(char* const p);  // 指针本身不可变,但是由于是形参,此处const无意义
    void fun(const A& a);   // 引用传递,同时也可以限制参数a 在函数体内不可改变,和形参不同之处在于 无需多一次拷贝
    
    • 修饰返回值
    const char *GetChar(void){};  // 表示该指针不能被改动,只能把该指针赋给const修饰的同类型指针变量。
    const char *ch=GetChar();  //此处const必不可少,只能返回const修饰的指针
    
    • 修饰函数体
    class Ex
    {
    public:
       int function(void) const ;  // 这里限制了在function函数体内不可以修改成员变量,否则编译阶段报错
    }
    

    static的几个问题:

    static的问题分类两大类

    static 修饰变量,面向过程中的问题

    • 静态全局变量
      静态全局变量的特点
      静态全局变量在全局区(静态区,参考本文开头第一部分)分配内存。
      静态全局变量的可见范围是本文件。
      应用本身如果没有初始化的话,静态变量会自动初始化为0。
    static int n;    // 编译成功后,由于静态全局变量不能再其他文件使用,所以该变量不能超出该文件
    void fn()
    {
        n++;
        cout<<n<<endl;    
    } 
    
    int main() {
        n = 10;  
        f();    // 编译成功后,由于静态全局变量不能在其他文件使用,所以该变量不能超出该文件
    }
    
    • 静态局部变量

    static修静态局部变量有以下特点:

    静态局部变量在全局数据区分配内存;
    静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
    静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
    静态局部变量始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

    void fn()
    {
        static n=10;  
           //  由于静态局部变量的存储空间在全局数据分配,而普通局部变量在栈上分配,
           //   所以即使本函数运行结束之后,n这个变量也一直保留,待到下次调用时可以继续使用该值,
           //   不需要额外的全局数据变量保存,相比全局变量而言,这个更容易控制
        cout<<n<<endl;
        n++;
    }
    
    • 静态函数
      静态函数的作用就是,该函数在本文件可见。
    static void fn();   //  声明静态函数
    void fn()   {
    }
    

    修饰类,在面向对象的问题

    • 静态成员变量
      在面向对象的语境下,静态成员变量意味着该数据只和类发生关系,不属于任何对象。
    class A{
        public:
            void f()   {
                cout<<"static member data : "<<s<<endl;
            }
        private:
            static int s;
    };
    
    int A::s = 10;   //  初始化可以不必有static修饰
    

    静态成员变量在全局数据区域分配内存,本类的所有对象共享,由于内存分配在全局的静态区内,所以他需要在初始化值,如果没有会默认初始化一个0。静态成员变量的存在和对象无关,只要定义类就有该变量分配的内存。 其他成员变量初始化如果没有指定,则是一个不可靠的值。
    private、protected、public 这方面的权限和其他成员变量一样。
    由于静态变量内存分配在全局静态区,不属于任何对象,所以,sizeof 运算符不会计算 静态成员变量。

    • 静态成员函数
    class A{
        public:
             void f()   {
                cout<<"static member data : "<<s<<endl;
            }
       
             static  void  sf()  {  // 如果分开写,后续可不再写static
                 }  
        private:
            static int s;
    };
    
    int A::s = 10;   //  初始化可以不必有static修饰
    

    关于静态成员函数:
    出现在类体外的函数定义不能指定关键字static;
    因为静态函数属于类,所以静态成员之间可以相互访问,即静态成员函数(仅)可以访问静态成员变量、静态成员函数;静态成员函数不能访问属于对象的非静态成员函数和非静态成员变量;
    非静态成员函数可以任意地访问静态成员函数和静态数据成员;
    没有this指针,静态成员函数与类的全局函数相比速度上会稍快;
    调用静态成员函数,可用:

    A a;
    a.sf();
    A::sf();
    

    这两种方式都可以调用静态成员函数。

    四、现代C++问题

    C++ 11增加了不少现代特性。

    右值引用

    现代C++11 以及之后的改进

    • 在C++98中,有如下代码:
    class RightRefEx { 
        public:
            RightRefEx()
            :d(new int(0)){ 
                cout<<"Construct:"<<++n_cstr<<endl;
            }
            
            RightRefEx(const RightRefEx &h)
              :d(new int(*h.d)){ 
                    cout<<"Copyconstruct:"<<++n_cptr<<endl;
             }
             
            ~RightRefEx() { 
                cout<<"Destruct:"<<++n_dstr<<endl;
            }
            
            int *d;
            
            static int n_cstr;
            static int n_dstr;
            static int n_cptr;
            };
            
            int RightRefEx::n_cstr = 0;
            int RightRefEx::n_dstr = 0;
            int RightRefEx::n_cptr = 0;
                
            RightRefEx GetTemp() {
                return RightRefEx();
            }
            
            int main()
            {
                RightRefEx a = GetTemp();
            }
    

    使用g++ rightRef.cpp -fno-elide-constructors编译。这里 -fno-elide-constructors表示编译器将采用RVO优化。也即是左值右值引用传递优化。
    一句RightRefEx a = GetTemp(); 调用了一次默认构造函数,两次拷贝构造函数,以及其中穿插了三次析构函数(具体结果可以自行运行并分析一下),实际项目中 一般不建议这么写。如果其中有大量的深拷贝的话,就会发生多次的内存分配 然后释放的情况。 这里也顺便给上一节中的静态成员变量的一个使用例子。

    在C++11中可以避免这些情况的存在,新的移动语义模型move semantics ,可以把一个对象中的已分配的内存直接传递给新的对象使用,而不需要重新分配在释放的过程。
    移动构造函数具体可参考上文中内存管理一节中 class String那个类的示例。

    • 左值 右值 右值引用的概念
      左值: 个有实际名字的,可以取地址操作的 比如 int a = b+c; ,&a 可以取地址,所以a是左值。
      右值:上述中 (b+c) 算是右值。
      发展到C++11之后,右值的概念有了进一步的发扬光大。右值分为将亡值 (xvalue)纯右值(prvalue)两层概念,在上述RightRefEx的例子中,多个拷贝构造函数以及 getTemp() 这样的函数返回时都构造了一些临时对象,这些临时对象可以理解为xvalue,但是如果将这个临时变量保存下来,其引用就是右值引用,如果有右值引用的话,这个临时变量将会一直存在,并且可以通过右值引用获得这个临时变量的值。比如上文中GetTemp()函数如果可以返回右值引用,那么构建出来的临时变量将会一直存在,然后用右值引用传递出来,这个值就可以表达出来, 并通过 a 获得值。
    RightRefEx && a = GetTemp();
    
    • std::move()的问题
      std::move() 可以将左值强制转换为右值。
      所以上述GetTemp()函数可以改为:
        RightRefEx && GetTemp() {
                return std::move(RightRefEx());
            }
    

    这时,只需要一次构造函数调用和一次析构函数调用了,减少了内存分配释放的消耗。

    <type_traits>里有不少可以判断的类型,//todo。

    • 完美转发 引用折叠
        void  f1(T t) {
            f2(t);
        }
    

    如果T不是基础数据类型,但部分情况我们都是用引用传递来传参数,这样的话,减少了很多拷贝开销。这里函数f1没有问题,但是对于f2的参数有一定的要求。 引用上引用折叠之后,可以这样设计

        void  f1(T && t) {
            f2(std::forward(t));
        }
    

    引用折叠todo。

    类型推导

    • auto

    • decltype

        int i;
        decltype(i) j = 0;        // i是int类型,推导出 decltype(i) 是int类型 
        cout << typeid(j).name() << endl;   // 打印出"i", g++表示integer
    
        float a;
        double b;
        decltype(a + b) c;
        cout << typeid(c).name() << endl;   // 打印出"d", g++表示double
    

    decltype 判断左值 右值问题时的规则。
    1)如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型。
    此外,如果e是一个被重载的函数,则会导致编译时错误。
    2)否则,假设e的类型是T,如果e是一个将亡值(xvalue),那么decltype(e)为T&&。
    3)否则,假设e的类型是T,如果e是一个左值,则decltype(e)为T&。
    4)否则,假设e的类型是T,则decltype(e)为T。

    • auto 和 decltype推导类型时,遇到cv(const, volatile)的类型推导规则: auto不可以“继承”, decltype可以“继承”。

    • POD type_traits // todo

    lambda表达式&仿函数

    lambda函数

    lambda的语法规则:
    [capture] (parameters)mutable -> returntype {statement}

    [capture]:捕捉列表。[]是lambda引出符。
    (parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,括号()可以一起省略。mutable:mutable修饰符。默认情况下,lambda函数是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)
    ->returntype:返回类型。用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号->一起省略。
    {statement}:函数体。
    极端情况下,C++11中最简略的lambda函数: []{};

       int main(){
        int a = 1;
        int b = 2;
        [=] { return a + b;};        // 省略了参数列表与返回类型,返回类型由编译器推断为int
        auto fun1 = [&](int c) { b = a + c; };  // 省略了返回类型,无返回值
        auto fun2 = [=, &b](int c)->int { return b += a + c; };     // 各部分都很完整
    }
    

    捕获列表的意思:
    ·[var]表示值传递方式捕捉变量var。
    ·[=]表示值传递方式捕捉所有父作用域的变量(包括this)。
    ·[&var]表示引用传递捕捉变量var。
    ·[&]表示引用传递捕捉所有父作用域的变量(包括this)。
    ·[this]表示值传递方式捕捉当前的this指针。

    仿函数

    • 仿函数 : 类的operator()被重载,行为也是一个函数
    class Price{
    private:
        float _rate;
    public: 
        Price(float rate): _rate(rate){}
        float operator()(float price) { 
            return price * (1 - _rate/100); 
        }
    };
    
    int main(){
        float trate = 5.5f;
        Price Hangi(tax_rate);
        auto Changi2 = 
            [trate](float price)->float{ return price * (1 - tax_rate/100); };
        float p1 = Hangi(3699);    // 仿函数
        float p2 = Changi2(2899);    // lambda表达式
    }
    

    五、 多线程问题

    C++11多线程的实现方式

    从C++11开始,C++开始在语言层面实现了多线程的,此前所有的多线程实现都极度依赖OS层面的实现。随着技术的发展,线程的支持逐步开始从操作系统层面到芯片层面的支持。

    1、 future

    • future 主要涉及到 promisepackaged_task两个template,另外还有一个async,、、todo

    2、 mutex互斥信号量

    • mutex 互斥信号量
    static long long total = 0;
    pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
    
    void* func(void *) {
        long long i;
        for(i = 0; i < 100000LL;i++) {   
            pthread_mutex_lock(&m);
            total += i;
            pthread_mutex_unlock(&m);
        }
    }
    
    int main() {
        pthread_t t1, t2;
        if (pthread_create(&t1, NULL, &func, NULL)){
            throw;
        }
        if (pthread_create(&t2, NULL, &func, NULL)){
            throw;
        }
        pthread_join(t1, NULL);
        pthread_join(t2, NULL);
        cout << total << endl; 
        return 0;
    }
    

    为了防止t1和t2竞争total这个资源而增加了一个mutex来控制访问。
    另外还可以用atomic来完成这个目的。

    六、原子类atomic

    • atomic
      atomic的定义
      先看下面一段代码
    void exchange(int ) {
        int a = 1;
        int b = a;
    }
    

    gcc asmex.cpp -lstdc++ -std=c++11 -S -o asm.s 得到汇编文件,未做优化,得到的代码如下(去掉不相关代码):

    __Z8exchangei:                          ## @_Z8exchangei
        .cfi_startproc
    ## %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        movl    %edi, -4(%rbp)       ##  形参
        movl    $1, -8(%rbp)           ## 给a赋值 1
        movl    -8(%rbp), %eax      ## 通过eax寄存器把a的值传递给b
        movl    %eax, -12(%rbp)
        popq    %rbp
        retq
        .cfi_endproc
    

    这里结构比较简略。

    源代码改atomic之后:

    void exchange(int ) {
        atomic<int> a {1};
        int b = a;
    }
    

    得到的汇编代码

    __Z8exchangei:                          ## @_Z8exchangei
        .cfi_startproc
    ## %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        subq    $16, %rsp
        movl    %edi, -4(%rbp)     ## 此前和前面一样,形参
        movl    L___const._Z8exchangei.a(%rip), %eax     ## rip是64位机器的指令寄存器,下一个要执行指令的存放地址。
        movl    %eax, -8(%rbp)    ;
        leaq    -8(%rbp), %rcx    ##  rcx 64位通常用来计数器的寄存器
        movq    %rcx, %rdi   ##   rdi 64位 字符串操作目的地址 和rsi一起 执行串复制
        callq   __ZNKSt3__113__atomic_baseIiLb0EEcviEv
        movl    %eax, -12(%rbp)
        addq    $16, %rsp
        popq    %rbp
        retq
        .cfi_endproc
    

    __ZNKSt3__113__atomic_baseIiLb0EEcviEv
    经过两次调用(这里略去这部分代码),参考源码atomic 中

    typedef __atomic_base<_Tp*> __base;
    .........
    _LIBCPP_CONSTEXPR atomic(_Tp* __d) _NOEXCEPT : __base(__d) {}
    

    其中
    和 base类 __atomic_base 的构造。然后到__atomic_base中, __base实现部分

    struct __cxx_atomic_impl : public _Base {
    
    #if _GNUC_VER >= 501
        static_assert(is_trivially_copyable<_Tp>::value,
          "std::atomic<Tp> requires that 'Tp' be a trivially copyable type");
    #endif
    
      _LIBCPP_INLINE_VISIBILITY __cxx_atomic_impl() _NOEXCEPT _LIBCPP_DEFAULT
      _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR explicit __cxx_atomic_impl(_Tp value) _NOEXCEPT
        : _Base(value) {}
    };
    

    然后,在汇编代码中最后调用到 :

    @_ZNSt3__1L17__cxx_atomic_loadIiEET_PKNS_22__cxx_atomic_base_implIS1_EENS_12memory_orderE
        .cfi_startproc
    ## %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        movq    %rdi, -8(%rbp)
        movl    %esi, -12(%rbp)
        movq    -8(%rbp), %rax
        movl    -12(%rbp), %ecx
        movl    %ecx, %edx
        decl    %edx
        subl    $2, %edx                 ## 从这里开始原子性的逻辑实现
            ## 后续大致逻辑就是符合一定要求的时候才可以进入指定的存储区域读取数据,
            ## 不然就要等待,这样确保了数据的一致性(本人解读,如有误还请读者指教)
        movq    %rax, -24(%rbp)         ## 8-byte Spill    rax通常执行加法的需要
        movl    %ecx, -28(%rbp)         ## 4-byte Spill    ecx通常做计数处理
        jb  LBB4_2
        jmp LBB4_5
    LBB4_5:
        movl    -28(%rbp), %eax         ## 4-byte Reload
        subl    $5, %eax
        je  LBB4_3
        jmp LBB4_1
    LBB4_1:
        movq    -24(%rbp), %rax         ## 8-byte Reload
        movl    (%rax), %ecx
        movl    %ecx, -16(%rbp)
        jmp LBB4_4
    LBB4_2:
        movq    -24(%rbp), %rax         ## 8-byte Reload
        movl    (%rax), %ecx
        movl    %ecx, -16(%rbp)
        jmp LBB4_4
    LBB4_3:
        movq    -24(%rbp), %rax         ## 8-byte Reload
        movl    (%rax), %ecx
        movl    %ecx, -16(%rbp)
    LBB4_4:
        movl    -16(%rbp), %eax
        popq    %rbp
        retq
        .cfi_endproc 
    

    以上是关于atomic变量的一些简单分析。

    • 内存模型
      为了确保atomic的原子性,隐含互斥量的使用降低了不少的效率:
    atomic<int> a {0};
    atomic<int> b {0};
    
    //int a = 0;
    //int b = 0;
    
    int ValueSet(int) {
        int t = 1;
          for(int i = 0; i < 1000000000ll; i++) {
            a++;b++;
        } 
    }
    
    int Observer(int) {
        cout << "(" << a << ", " << b << ")" << endl;   // 输出不确定,但是atomic<int>的类型会比int小很多
    }
    
    int main() {
        thread t1(ValueSet, 0);
        thread t2(Observer, 0);
    
        t2.join();
        t1.join();
        cout << "Got (" << a << ", " << b << ")" << endl;   
       // 运行到这,定义atomic<int> 运行时长超过int类型数据不少,具体和机型有关
    }
    

    可以明显看出atomic<T> 为了确保原子性,做出了很大性能牺牲,其中之一就是原子类型数据是不可以乱序执行的,在对性能有高要求的情况下,需要更有效的使用这些变量,又要确保不会由于编译器优化导致的非预期结果,比如可以要求部分情况下原子变量可以被并行执行,可以乱序, 其中涉及到内存模型 memory-order的问题。

    引入一个 强顺序和弱顺序的内存模型。
    以X86位代表的芯片就是强顺序模型,所以强顺序,就是生成的汇编指令在执行的时候,按照我们看到的顺序执行,不会乱。
    以PowerPC 、ArmV7位代表的弱顺序模型,生成的汇编指令在执行的时候,会被优化后,没有按照原先设定的顺序执行,弱顺序内存模型的出现主要是为了提高指令的执行效率。
    对于弱顺序执行的处理器而言,为了保证执行的顺序,在指令中加入了一个内存栅栏(memory barrier,也有翻译成内存屏障的),一般用sync表示。

    代码表示如下:

    a.store(t,memory_order_relaxed);
    

    原子存储操作(store)可以使用memorey_order_relaxed、memory_order_release、memory_order_seq_cst。
    原子读取操作(load)可以使用memorey_order_relaxed、memory_order_consume、memory_order_acquire、memory_order_seq_cst。

    memorey_order
    • 线程局部存储TLS
    int thread_local errorCode;
    

    C++11规定了这种变量归于线程。但是其实现给该变量分配内存,如何分配和管理与编译器本身的实现有关。

    相关文章

      网友评论

          本文标题:C++(98/11)问题 (1)

          本文链接:https://www.haomeiwen.com/subject/maqqtltx.html