《深入理解C++11》笔记(第六章. 提高性能及操作硬件的能力)

今天二刷《深入理解C++11》,就顺带把我在印象笔记的摘录传到CSND上,禁止转载!!!

全部笔记链接:

提高性能及操作硬件的能力

1 常量表达式

1.1 运行时常量性与编译时常量性
  • const 修饰的只具有运行时常量性,不过有的时候,我们需要的是编译期常量
const int GetConst() { return 1; }
void Constless(int cond) {
  int arr[GetConst()] = {0};      // 无法通过编译
  enum { e1 = GetConst(), e2 };  // 无法通过编译
  switch (cond) {
            case GetConst():             // 无法通过编译
                break;
            default:
                break;
        }
  }
  // 编译选项:g++ -c 6-1-1.cpp
  • C++11中对编译时期常量的回答是constexpr,即常量表达式(constant expression)。
1.2 常量表达式函数
  • 常量表达式函数的要求非常严格,总结起来,大概有以下几点:
    1. 函数体只有单一的return返回语句。
      • 一些不会产生实际代码的语句,在常量表达式函数中使用下,倒不会导致编译器的“抱怨”,比如:
        constexpr int f(int x){
        static_assert(0 == 0, “assert fail.”);
        return x;
        }
      • 其它的诸如:using、typedef等也通常不会造成问题。
    2. 函数必须有返回值(不能是void函数)。
    3. 在使用前必须已有定义。
      • 对于普通函数而言,调用函数只需要有函数声明就够了;
      • 应该注意常量表达式“使用”和“调用”的区别,前者讲的是编译时的值计算,而后者讲的是运行时的函数调用
      • constexpr int f();
        int a = f();
        const int b = f();
        constexpr int c = f(); // 无法通过编译
        constexpr int f() { return 1; }
        constexpr int d = f();
        // 编译选项:g++ -std=c++11-c 6-1-3.cpp
        解释:在a和b的定义中,编译器会将f()转换为一个函数调用。 虽然f被定义为一个常量表达式,但是它是否在编译时进行值计算则不一定。
      • 如下面的声明就会导致编译器报错:
        constexpr int f();
        int f();
    4. return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式,一些危险的操作,比如赋值在常量表达式中也是不允许的。
1.3 常量表达式
  • 通常情况下,常量表达式值必须被一个常量表达式赋值,而跟常量表达式函数一样,常量表达式值在使用前必须被初始化。
  • const int i = 1 与 constexpr int i = 1的区别
    事实上,两者在大多数情况下是没有区别的。不过有一点是肯定的,就是如果i在全局名字空间中,编译器一定会为i产生数据。而对于j,如果不是有代码显式地使用了它的地址,编译器可以选择不为它生成数据,而仅将其当做编译时期的值(是不是想起了光有名字没有产生数据的枚举值,以及不会产生数据的右值字面常量?事实上,它们也都只是编译时期的常量)。
  • 浮点常量
    通常情况下,编译器对浮点数做编译时期常量这件事情很敏感。因为编译时环境和运行时环境可能有所不同,那么编译时的浮点常量和实际运行时的浮点数常量可能在精度上存在差别。 不过在C++11中,编译时的浮点数常量表达式值还是被允许的。标准要求编译时的浮点常量表达式值的精度要至少等于(或者高于)运行时的浮点数常量的精度。
  • C++11标准中,constexpr关键字是不能用于修饰自定义类型的定义的:
    constexpr struct MyType {int i; }
    constexpr MyType mt = {0};

    在C++11中,就是无法通过编译的。

  • 正确地做法是,定义自定义常量构造函数(constent-expression constructor)。注:与上类似,这里不需要再定义非常量构造函数。
struct MyType {
  constexpr MyType(int x): i(x){}
  int i;
};
constexpr MyType mt = {0};
// 编译选项:g++ -c -std=c++11 6-1-4.cpp

常量表达式的构造函数也有使用上的约束,主要的有以下两点:
* 函数体必须为空。
* 初始化列表只能由常量表达式来赋值。

  • 在C++11中,不允许常量表达式作用于virtual的成员函数。

原因也是显而易见的,virtual表示的是运行时的行为,与“可以在编译时进行值计算”的constexpr的意义是冲突的。

1.4 常量表达式的其他应用
  • 用于模版函数

由于模板中类型的不确定性,所以模板函数是否会被实例化为一个能够满足编译时常量性的版本通常也是未知的。针对此种情形,C++11标准规定,当声明为常量表达式的模板函数后,而某个该模板函数的实例化结果不满足常量表达式的需求的话,constexpr会被自动忽略

  • 函数递归问题

C++11标准中并没有反对常量表达式的递归函数,而且在标准中说明,符合C++11标准的编译器对常量表达式函数应该至少支持512层的递归。

#include <iostream>
using namespace std;
constexpr int Fibonacci(int n) {
    return (n == 1) ? 1 : ((n == 2) ? 1 : Fibonacci(n -1) + Fibonacci(n -2));
}
int main() {
    int fib[] = {
        Fibonacci(11), Fibonacci(12),
        Fibonacci(13), Fibonacci(14),
        Fibonacci(15), Fibonacci(16)
    };
    for (int i : fib) cout << i << endl;
}
// 编译选项:clang++ -std=c++11 6-1-7.cpp
  • 模板元编程 & constexpr元编程
#include <iostream>
using namespace std;
template <long num>
struct Fibonacci{
    static const long val = Fibonacci<num -1>::val + Fibonacci<num -2>::val;
};
template <> struct Fibonacci<2> { static const long val = 1; };
template <> struct Fibonacci<1> { static const long val = 1; };
template <> struct Fibonacci<0> { static const long val = 0; };
int main() {
    int fib[] = {
        Fibonacci<11>::val, Fibonacci<12>::val,
        Fibonacci<13>::val, Fibonacci<14>::val,
        Fibonacci<15>::val, Fibonacci<16>::val,
    };
    for (int i : fib) cout << i << endl;
}
// 编译选项:g++ -std=c++11 6-1-8.cpp

这里牵扯到类型模版参数参数偏特化模板元编程 TODO;整理出来

通过constexpr进行的运行时值计算,跟模板元编程非常类似。因此有的程序员自然地称利用constexpr进行编译时期运算的编程方式为constexpr元编程(constexpr meta-programming)。学术地讲,constexpr元编程与template元编程一样,都是图灵完备的,即任何程序中需要表达的计算,都可以通过constexpr元编程的方式来表达。由于constexpr支持浮点数运算(模板元编程只支持整型),支持三元表达式、逗号表达式,所以很多人认为constexpr元编程将会比模板元编程更加强大。从这个角度讲,constexpr元编程将非常让人期待。

但是,并不是使用了constexpr,编译器就一定会在编译期对常量表达式函数进行值计算。 事实上,对于代码清单6-4所示的例子,如果用g++的默认优化级别来编译,我们实验机上会产生调用Fibonacci函数的代码(clang++在O0级别也会有这样的效果)。在C++11标准中,我们也没有看到要求编译器一定要对常量表达式进行编译时期的值计算。标准只是定义了可以用于编译时进行值运算的常量表达式的定义,却没有强制要求编译器一定在编译时进行值运算。所以编译器通过一些手段绕过代码的语法,仍然做运行时的调用,这样的方法也是符合C++11标准的(通常情况下,这样做也是编译器实现constexpr的第一步,因为这样最简单也最不容易出错,后期如果实现了编译时值计算,该方法还可以用作验证手段)。推迟到运行时的唯一的缺点就是性能上会有一定问题。可以想象,为了提高编译器的竞争力,各种编译器都会渐渐将常量表达式的运算放到编译时,到那个时候,constexpr元编程或许才能大行其道。

2 变长模板

2.1 变长函数和变长的模板参数
  • 变长函数的例子
#include <stdio.h>
#include <stdarg.h>
double SumOfFloat(int count, ...) {
    va_list ap;
    double sum = 0;
    va_start(ap, count);              // 获得变长列表的句柄ap
    for(int i = 0; i < count; i++)
        sum += va_arg(ap, double);  // 每次获得一个参数
    va_end(ap);
    return sum;
}
  int main() {
      printf("%f\n", SumOfFloat(3, 1.2f, 3.4, 5.6));   // 10.200000
  }
  // 编译选项:gcc 6-2-1.cpp

函数“本身”完全无法知道参数数量或者参数类型。因此,对于一些没有定义转义字的非POD的数据来说,使用变长函数就会导致未定义的程序行为。比如:

      const char *msg = "hello %s";
      printf(msg, std::string("world"));

这样的代码就会导致printf出错。

  • tuple && pair
    std::tuple<double, char, std::string> collections;
    std::make_tuple(9.8, 'g', "gravity");

    在C++11中,tuple是pair类的一种更为泛化的表现形式。比起pair,tuple是可以接受任意多个不同类型的元素的集合。

    在C++98中,由于没有变长模板,tuple能够支持的模板参数数量实际上是有限的。 这个数量是由标准库定义了多少个不同参数版本的tuple模板而决定的。

2.2 变长模板:模板参数包和函数参数包
#include <iostream>
using namespace std;
template <long... nums> struct Multiply;
template <long first, long... last>
struct Multiply<first, last...> {
    static const long val = first * Multiply<last...>::val;
};
template<>
struct Multiply<> {
    static const long val = 1;
};
int main() {
    cout << Multiply<2, 3, 4, 5>::val << endl;             // 120
    cout << Multiply<22, 44, 66, 88, 9>::val << endl;    // 50599296
    return 0;
}
// 编译选项:clang++ -std=c++11 6-2-3.cpp
  • 在C++11中,标准要求函数参数包必须唯一,且是函数的最后一个参数(模板参数包没有这样的要求)。

  • 使用模版参数包和函数参数包实现C中变长函数的功能(见2.1中例子)

#include <iostream>
#include <stdexcept>
using namespace std;
void Printf(const char* s) {
    while (*s) {
        if (*s == '%' && *++s != '%')
            throw runtime_error("invalid format string: missing arguments");
        cout << *s++;
    }
}
template <typename T, typename... Args>
void Printf(const char* s, T value, Args... args) {
    while (*s) {
        if (*s == '%' && *++s != '%') {
            cout << value;
            return Printf(++s, args...);
        }
        cout << *s++;
    }
    throw runtime_error("extra arguments provided to Printf");
}
int main() {
    Printf("hello %s\n", string("world"));   // hello world(注:运行结果与%后面的字符没关系)
}
// 编译选项:g++ -std=c++11 6-2-4.cpp

相比于变长函数,变长函数模板不会丢弃参数的类型信息。因此重载的cout的操作符<<总是可以将具有类型的变量正确地打印出来。这就是Printf功能大于printf的主要原因,也是变长模板函数远强于变长函数的地方。

2.3 变长模版:进阶

TODO:再看看,好好整理

3 原子类型与原子操作

3.1 并行编程、多线程与C++11
  • 常见的并行编程有多种模型,如共享内存、多线程、消息传递等。不过从实用性上讲,多线程模型往往具有较大的优势。
  • 在C++11之前,在C/C++中程序中使用线程却并非鲜见。这样的代码主要使用 POSIX线程(Pthread)OpenMP编译器指令 两种编程模型来完成程序的线程化。

    POSIX线程是POSIX标准中关于线程的部分,程序员可以通过一些Pthread线程的API来完成线程的创建、数据的共享、同步等功能。Pthread主要用于C语言,在类UNIX系统上,如FreeBSD、NetBSD、OpenBSD、GNU/Linux、Mac OS X,甚 至 是Windows上 都 有 实 现(Windows上Pthread的实现并非“原生”,主要还是包装为Windows的线程库)。不过在使用的便利性上,Pthread不如后来者OpenMP。

3.2 原子操作与C++11原子类型
  • 所谓原子操作,就是多线程程序中“最小的且不可并行化的”的操作。
  • 通常情况下,原子操作都是通过“互斥”(mutual exclusive)的访问来保证的。
    实现互斥通常需要平台相关的特殊指令,这在C++11标准之前,这常常意味着需要在C/C++代码中嵌入内联汇编代码。对程序员来讲,就必须了解平台上与同步相关的汇编指令。当然,如果只是想实现粗粒度的互斥,借助POSIX标准的pthread库中的互斥锁(mutex)也可以做到。
#include <pthread.h>
#include <iostream>
using namespace std;
static long long total = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
void* func(void *) {
    long long i;
    for(i = 0; i < 100000000LL;i++) {
        pthread_mutex_lock(&m);
        total += i;
        pthread_mutex_unlock(&m);
    }
}
int main() {
    pthread_t thread1, thread2;
    if (pthread_create(&thread1, NULL, &func, NULL)){
        throw;
    }
    if (pthread_create(&thread2, NULL, &func, NULL)){
        throw;
    }
    pthread_join(thread1, NULL);
      pthread_join(thread2, NULL);
      cout << total << endl;   // 9999999900000000
      return 0;
  }
  // 编译选项:g++ 6-3-1.cpp -lpthread
3.3 内存模型,顺序一致性与memory_order
  • C++11标准中一共定义了6种memory_order的枚举值
    在这里插入图片描述

memory_order_seq_cst表示该原子操作必须是顺序一致的,这是c++11中所有atomic原子操作的默认值。

  • 并非每种memory_order都可以被atomic的成员使用。通常情况下,可以分成3组:
    • 原子存储操作(store)可以使用
      memory_order_relaxed
      memory_order_release
      memory_order_seq_cst
    • 原子读取操作(load)可以使用
      memory_order_relaxed
      memory_order_consume
      memory_order_acquire
      memory_order_seq_cst
    • RMW操作(read-modify-write),即一些需要同时读写的操作,比如之前提过的atomic_flag类型的test_and_set()操作。
      memory_order_relaxed
      memory_order_consume
      memory_order_acquire
      memory_order_release
      memory_order_acq_rel
      memory_order_seq_cst
#include <thread>
#include <atomic>
#include <iostream>
using namespace std;
atomic<int> a;
atomic<int> b;
int Thread1(int) {
       int t = 1;
       a.store(t, memory_order_relaxed);
       b.store(2, memory_order_relaxed);
}
int Thread2(int) {
       while(b.load(memory_order_relaxed) != 2);  // 自旋等待
       cout << a.load(memory_order_relaxed) << endl;
}
int main() 
{
       thread t1(Thread1, 0);
       thread t2(Thread2, 0);
       t1.join();
       t2.join();
       return 0;
}
// 编译选项:g++ -std=c++11 6-3-7.cpp -lpthread

4 线程的局部存储

线程局部存储(TLS, thread local storage)是一个已有的概念。简单地说,所谓线程局部存储变量,就是拥有线程生命期及线程可见性的变量。
C++11的语法如下:
int thread_local errCode;
虽然TLS变量的声明很简单,使用也很简单,但是TLS的实现需要涉及编译器、链接器、加载器甚至是操作系统的相互配合。
还有一点值得注意的是,C++11对TLS只是做了语法上的统一,而对其实现并没有做任何性能上的规定。这可能导致thread_local声明的变量在不同平台或者不同的TLS实现上出现不同的性能(通常TLS变量的读写性能不会高于普通的全局/静态变量)。

5 快速退出:quick_exit与at_quick_exit

  • termiante
    有些内部实现就是直接调用abort
  • abort
    abort是系统在毫无办法下的下下策:终止进程。
  • exit
  • quick_exit
    一项多线程情况下的新发明,可以用于解除因为退出造成的死锁等不良状态。不过读者也可以尝试着使用它来免除大量的不必要的析构函数调用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值