C++开发的一些经验和踩坑整理
STL相关的坑
1. std::sort()函数要求严格弱序
STL文档中要求sort实现的函数要遵守严格弱序的规则。使用的时候,稍有不慎就会可能忘记,程序跑起来的时候,会导致容器越界,从而使程序coredump。google搜了一下,上面无数的人在这个问题上掉坑,stl的bug feature。
判断自反性一个很简单的标准是:
cmp(x,x) 如果等于true的话,那肯定是有问题的。
提供一个coredump的案例,以下是一个反例,可以看出来是为什么?
struct FOO {
int id1;
int id2;
int id3;
};
bool _sort_cmp(const FOO& item1
, const FOO& item2) {
return item1.id1 <= item2.id1;
}
int main() {
FOO item = {1, 2, 3};
vector vec;
for (int i = 0; i < 50; i++){vec.push_back(item);}
std::sort(vec.begin(), vec.end(), _sort_cmp);
return 0;
}
--------
$ ./a.out
[1] 3151 segmentation fault (core dumped) ./a.out
2. std::list中的size方法实现是O(n)的
C++11之前的std::list中的size方法实现是O(n)的。如果list中的元素特别巨大,而且要频繁获得size的时候,要注意程序的性能了。如果一定要用的话,可以采用使用deque。
const int max_size = 1000000;
const int append_size = 10000;
template
void test_func1(T &test_list)
{
for (int i = 0; i < append_size; i++)
{
int size = test_list.size();
test_list.push_back(size);
}
return;
}
int main()
{
list test_list;
deque test_deque;
for (int i = 0; i < max_size; i++)
{
test_list.push_back(i);
test_deque.push_back(i);
}
test_func1(test_list);
// test_func1(test_deque);
return 0;
}
------
从时间上面看,执行时间的差距非常非常大。
# 执行 test_list
$ time ./a.out
./a.out 60.07s user 0.02s system 99% cpu 1:00.16 total
# 执行 test_deque
$ time ./a.out
./a.out 0.10s user 0.02s system 98% cpu 0.130 total
同理,对于需要判断容器是不是为空,更加推荐用empty()方法,而不是size()方法。
3. vector有坑
vector
4. 字节对齐的坑
todo
5. 父类子类对象转换的坑
todo
6. insert和earse之后,原有的迭代器有可能失效
对于insert和erase对于容器的操作, 必须假设都会使所有迭代器失效。尤其是erase之后,忘记了的话,非常容易coredump了。
7. 合理使用stl
reserve()
vector的空间是2的倍数增长的,每次空间扩容的时候,都需要重新拷贝原来的元素。如果已经可以确定容器需要的元素的数量,那可以提前用reserve()预定容量,stl会把capacity一次分配到位。利用swap缩容,string(s).swap(s);
从vector中删除元素缩减了该vector的大小(size),但是并没有减小它的容量(capacity)。clear()并不会导致空间收缩 ,如果需要释放空间,可以跟空的vector交换,std::vector.swap(v),c++11里shrink_to_fit()也能收缩内存。
vector().swap(_vectorToBeReleased);
- 大的对象插入vector要注意开销
首先,insert或者push_back方法插入容器的都是原来对象的拷贝;其次,每次容器空间扩容的时候,会重新拷贝原来的对象。可以尝试改为指针,或者可以去掉一些冗余的数据,或者使用emplace_back方法。
如果你的map中的元素, 不需要被排序, 那么考虑使用hashmap或者tr1中的unordered_map, 这样整体插入及查询删除效率都会有很大的提升
vector, list, deque的容器选择问题
vector是堆上的连续数组, list是双向链表, deque是多个连续内存块:
a) vector的最佳选择是知道容器中元素个数, 然后调用reserve方法, push_back效率是三者最高的, 且这样不涉及到的内存的复制及重新分配问题
b) 需要频繁的在任意位置插入和删除, 选择list, 毫无疑问
c) 只是在容器的头部和尾部做插入和删除, 那么尽量选择deque, 否则选择vector-
合理使用vector
- vector的本质是数组,当从0开始push_back的时候,超过了数组的上限会重新开辟空间。也就意味着会有对象迁移的成本。如果预知了对象的数量,可以reserve一下。
- 当不同的从vector里面pop元素,数组会有空间浪费,可以swap一下,或者fit一下,会收缩内存。
- at()会做下标越界检查,operator[]提供数组索引级的访问
合理使用map
map对应的数据结构是红黑树,unoder_map是hash表。(这也很好理解,因为底层是hash,所以不能遍历保证有序)。hash表比红黑树有更高的查找性能。和vector类似,map从0开始添加元素,会导致hash表冲突的可能性越来越高。于是,触发rehash。rehash是需要的时间成本的。
-
其他的坑:
class A
class B : public A
vector.push_back(inst_B),
结果只会push_back inst_B中的A实例部分, B到A的派生出来的部分会被剔除
解决方案是使用容器vector>来保存B对象的指针即可, 但是注意请自行析构vector>容器中的元素如果为指针, 请自行进行删除指针, 如vector
, 因为指针不能被析构掉
- stl线程不安全
C++开发相关注意的问题
0. 一些细节问题
float val = 1/10; // val是多少?
val是0
int8_t val = 500;
val输出为什么不是500?
这种小问题单拿出来一眼就可以看出来问题,但是在一个复杂的程序上下文里面就不一定很容易找到了。
1. 栈上定义大结构体
栈空间很有限,不能在栈上定义过大的临时对象。一般而言,用户栈只有几兆(典型大小是4M,8M),所以栈上创建的对象不能太大。如果一定要加,那么带上static。如果过大了,会怎么样? 掉坑详情:https://www.jianshu.com/p/af28c76f7a28
2. 全局变量的初始化顺序
effective c++里面也提到过这个问题,全局变量的初始化顺序是不能够保证的。
同样,析构的时候也会有一个潜在的问题。如果A比B先构造,那么结束的时候,B先析构,A后析构。但是,如果A析构的时候,依赖于B。那么,就出问题了。
如果全局变量是有顺序的,那么就不要放在不同的文件中,或者写一个单独的逻辑来控制init和finish的顺序。
3. 给局部变量和成员变量赋初值
不要寄希望于变量会被默认初始化为0,编译器在级别的优化下或debug模式下,会给变量初始化为0。但是,不能指望编译器在任何时候都会做这件事情。
4. 内存拷贝小心内存越界;memcpy,memset有很强的限制,仅能用于POD结构,不能作用于stl容器或者带有虚函数的类
- memset等操作要注意操作的对象和它的size是否一致等
- memset仅能用于POD结构
5. 注意循环的边界
常见的for,while循环内没有index++,或者循环内走了每个分支就不会index++ ; do{}while(true) ; do{}while(恒为true)等。
// 典型反例
for (uint32_t i = 10; i >= 0; i--)
{
// dosomething();
}
std::vector vec;
vec.push_back(1);
for (auto idx = vec.size(); idx >= 0; idx--) {
cout << "===== \n";
}
5. || 运算符会被优化
以下这两段代码是有区别的,对于 a||b
运算符,如果计算到a是true的时候,就不会在执行b了。
// 片段1
if ( do_a() || do_b())
{
// do_c()
}
// 片段2
bool result_a = do_a();
bool result_b = do_b();
if ( result_a || result_b)
{
// do_c();
}
# 对于片段1 如果do_a是true的话,do_b就不会执行了
6. 使用延迟计算或者被动更新,避免重复计算
加入一个值,依赖于其他100个值。当100个值中的任意一个更新之后,这个值都需要重新计算。
那么,可以使用延迟计算,先设一个dirty的标记,使用的时候,在重新计算。
7. 用c标准库的安全版本(带n标识)替换非安全版本
比如用strncpy替代strcpy,用snprintf替代sprintf,用strncat代替strcat,用strncmp代替strcmp,memcpy(dst, src, n)要确保[dst,dst+n]和[src, src+n]都有有效的虚拟内存地址空间。;多线程环境下,要用系统调用或者库函数的安全版本代替非安全版本(_r版本),谨记strtok,gmtime等标准c函数都不是线程安全的。
C++性能上优化的小技巧
-
cpu局部性原理
for (int i = 0; i < size; i++) { for (int j = 0; j < 8; j++) { sum = arr[i][j]; } } for (int i = 0; i < 8; i += 1) { for (int j = 0; j < size; j++) { sum = arr[j][i]; } }
按列访问会打破局部性的原理,会导致cpu cache更多的miss,导致性能下降。
- 伪共享
系统中是以缓存行(cache line)为单位存储的,缓存行通常是 64 字节。如果有多个线程操作不同的成员变量,但是相同的缓存行,这个时候会发生什么?。没错,伪共享(False Sharing)问题就发生了。
-
乘法比除法的cpu指令周期更少
可以把除法运算改为乘法运算if (a/100 <= b) if (a <= b* 100)
成本比较高的系统调用的结果可以缓存起来,再一个可以容忍的有效期之内使用缓存的结果。比如glog调用gettimeofday的时候,如果在同一个时间间隔内,就用缓存,而不是在调用一次。