一个std::sort 自定义比较排序函数 crash的分析过程
两年未写总结博客,今天先来练练手,总结最近遇到的一个crash case。
注意:以下的分析都基于GCC4.4.6
一、解决crash
我们有一个复杂的排序,涉及到很多个因子,使用自定义排序函数的std::sort做排序。Compare函数类似下文的伪代码:
bool compare(const FakeObj& left, const FakeObj& right) {
if (left.a != right.a) {
return left.a > right.a;
}
if (left.b != right.b) {
return left.b > right.b;
}
....
}
后来,我们给排序函数加了更多的复杂逻辑:
bool compare(const FakeObj& left, const FakeObj& right) {
if (left.a != right.a) {
return left.a > right.a;
}
if (left.b != right.b) {
return left.b > right.b;
}
if (left.c != && right.c != && left.c != right.c) {
// 当C属性都存在的时候使用C属性做比较
return left.c > right.c;
}
if (left.d != right.d) {
return left.d > right.d;
}
....
}
服务发布之后,进程就开始出现偶现的crash,使用gdb查看,调用堆栈如下:
/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_algo.h:5260
/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_algo.h:2194
/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_algo.h:2161
/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_algo.h:2084
crash发生位置:在标准库调用compare函数执行比较的时候出现了越界:
这时候,开始怀疑compare函数没有按照标准库的规范实现,查看相关资源:
https://en.cppreference.com/w/cpp/named_req/Compare
仔细看官方的文档可以发现:
我们的compare函数对c属性的判断,没有严格遵守可传递性:if comp(a,b)==true and comp(b,c)==true then comp(a,c)==true。假设存在A、B、C三个对象,
1、A、B对象有属性c,且A.c > B.c,按照我们的比较函数,这时候A>B;
2、C对象没有c属性,且C.d>A.d,这时候C>A;
3、C对象没有c属性,且B.d < C.d,这时候B>C
综上,A>B 且 B>C,但是C>A,这就违反了strict weak ordering的transitivity。
到这里,我们的case就解决了,但实际上,基于以下几个原因,这个case花费了很长的时间:
1、 我们的compare函数的代码不是逐步添加的,而是一次性写完,导致没有立即怀疑c属性的比较有bug;
2、 对官方文档不够重视,只关注到了非对称性:comp(a,b) ==true then comp(b,a)==false,忽略了可传递性;
辗转了很久才注意到传递性要求。后续在解决问题时,应该更细致,不放过每一个细节。
二、crash更深层的原因
业务上的crash问题已经解决,但crash的直接原因是什么还是未知的,需要继续探索。
找到std::sort的源码:
https://github.com/gcc-mirror/gcc/blob/gcc-4_4-branch/libstdc%2B%2B-v3/include/bits/stl_algo.h
再结合其他人分析std::sort源码的总结:
https://www.cnblogs.com/AlvinZH/p/8682992.html
https://liam.page/2018/09/18/std-sort-in-STL/
简单的总结:std::sort为了提高效率,综合了快排、堆排序、插入排序,可以分为两阶段:
1、 快排+堆排序(__introsort_loop),对于元素个数大于_S_threshold的序列,执行快排,当快排的递归深入到一定层次(__depth_limit)时,不再递归深入,对待排序元素执行堆排序;对于元素个数小于_S_threshold的序列则不处理,交给后面的插入排序。
2、 插入排序(__final_insertion_sort),当元素个数小于_S_threshold时,执行普通的插入排序(__insertion_sort);当大于_S_threshold时,执行两批次的插入排序,首先是普通的插入排序排[0, _S_threshold);然后是无保护的插入排序(__unguarded_insertion_sort),从_S_threshold位置开始排,直到end,注意这里可能还会处理到_S_threshold之前的元素(因为这个函数只用比较结果来判断是否停止,而不强制要求在某个位置点上停止)。
我们的crash发生在__unguarded_insertion_sort阶段,也就是无保护的插入排序。看下这块的代码:
/// This is a helper function for the sort routine.
template<typename _RandomAccessIterator, typename _Compare>
inline void __unguarded_insertion_sort(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Compare __comp)
{
typedef typename iterator_traits<_RandomAccessIterator>::value_type _ValueType;
for (_RandomAccessIterator __i = __first; __i != __last; ++__i)
std::__unguarded_linear_insert(__i, _ValueType(*__i), __comp);
} /// This is a helper function for the sort routine.
template<typename _RandomAccessIterator, typename _Tp, typename _Compare>
void __unguarded_linear_insert(_RandomAccessIterator __last, _Tp __val,
_Compare __comp) {
_RandomAccessIterator __next = __last;
--__next;
while (__comp(__val, *__next)) {
*__last = *__next;
__last = __next;
--__next;
}
*__last = __val;
}
可以看到,__unguarded_linear_insert 函数比较的终止条件是compare函数返回false,否则就一直排序下去,这里之所以可以这么做,是因为之前的快排+堆排代码保证了[0,X)序列的元素肯定大于(假设是递减排序)[X, end),其中0<X<=_S_threshol,一旦无法保证,则会导致--__next越界,最终导致crash。
再回到我们的crash case,因为compare函数不满足传递性,虽然[0,X)区间的所有元素都大于X,且(X,end]区间的所有元素都小于X,但是并不能保证(X,end]的元素都小于[0,X)区间的元素,在__unguarded_linear_insert函数里,对(X,end]区间的元素执行插入排序时, 某元素大于[0,X)区间的所有元素,这时候就发生了越界crash。
这里使用__unguarded_insertion_sort而不是仅使用__insertion_sort的好处是可以节省边界判断。相关讨论:https://bytes.com/topic/c/answers/819473-questions-about-stl-sort
一个std::sort 自定义比较排序函数 crash的分析过程的更多相关文章
- KMP算法的next函数求解和分析过程
转自 wang0606120221:http://blog.csdn.net/wang0606120221/article/details/7402688 假设KMP算法中的模式串为P,主串为S,那么 ...
- ptyhon 编程基础之函数篇(二)-----返回函数,自定义排序函数,闭包,匿名函数
一.自定义排序函数 在Python中可以使用内置函数sorted(list)进行排序: 结果如下图所示: 但sorted也是一个高阶函数,可以接受两个参数来实现自定义排序函数,第一个参数为要排序的集合 ...
- 冒泡排序和sort,sorted排序函数
冒泡: # 轮数 元素个数 比较次数# 1 6 5# 2 5 4# 3 4 3# 4 3 2# 5 2 1 # 列表有n个元素,则应比较n-1轮,即循环次数n-1 a=[85,7,4,89,34,2] ...
- PHP常用数字函数以及排序函数
一:数字函数 .ceil() 进一取整 示例:ceil(0.9) 结果为1 .abs() 绝对值 示例:abs(-1) 结果为1 .rand() 随机数 示例:rand(1. 100) 1到100 以 ...
- c++中std::set自定义去重和排序函数
c++中的std::set,是基于红黑树的平衡二叉树的数据结构实现的一种容器,因为其中所包含的元素的值是唯一的,因此主要用于去重和排序.这篇文章的目的在于探讨和分享如何正确使用std::set实现去重 ...
- std list/vector sort 自定义类的排序就是这么简单
所以,自己研究了一下,如下:三种方式都可以,如重写<,()和写比较函数compare_index.但是要注意对象和对象指针的排序区别. 1.容器中是对象时,用操作符<或者比较函数,比较函数 ...
- (C++)STL排序函数sort和qsort的用法与区别
主要内容: 1.qsort的用法 2.sort的用法 3.qsort和sort的区别 qsort的用法: 原 型: void qsort(void *base, int nelem, int widt ...
- C++ 中的sort()排序函数用法
sort(first_pointer,first_pointer+n,cmp) 该函数可以给数组,或者链表list.向量排序. 实现原理:sort并不是简单的快速排序,它对普通的快速排序进行了优化,此 ...
- hdu 1263 水果 结构的排序+sort自定义排序
水果 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submissi ...
随机推荐
- PHP递归获取二维数组中指定key的值
$data = [ "resulterrorCode" => 0, "resultraw" => [ "result" => ...
- .NET Core on K8S快速入门课程学习笔记
课程链接:http://video.jessetalk.cn/course/explore 良心课程,大家一起来学习哈! 目录 01-介绍K8s是什么 02-为什么要学习k8s 03-如何学习k8s ...
- C# 通过 Quartz .NET 实现 schedule job 的处理
在实际项目的开发过程中,会有这样的功能需求:要求创建一些Job定时触发运行,比如进行一些数据的同步. 那么在 .Net Framework 中如何实现这个Timer Job的功能呢? 这里所讲的是借助 ...
- labview下载地址
ftp://ftp.ni.com/evaluation/labview/ekit/other/downloader
- Y1S001 ubuntu下samba安装配置以及使用vbs映射到驱动器
我这边安装samba只用了两步 第一步 sudo apt-get install samba 第二步 sudo vi /etc/samba/smb.conf 主要修改点如下,去掉注释或者修改=右边的值 ...
- 使用curl上传图片的方法
关键:当参数名为"@绝对路径",这时 CURL 會幫你做 multipart/form-data 編碼 实现方法: $params = array( 'file' => '@ ...
- redis的过期策略都有哪些?
1.面试题 redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2.面试官心里分析 1)老师啊,我往redis里写的数据怎么没了? 之前有同学问过我,说我们生产环境的redi ...
- Tips_发送请求时添加一个随机数参数,让浏览器每次都重新发请求到服务器
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 使用Jacksum对文件夹和文件生成checksum
Jacksum 是一个java开源工具, 用来 给单个文件生成checksum, 也可以给整个文件中所有文件生成checksum,生产的checksum 可以是MD系列,也可sha. 你可以参考 官 ...
- 封装ajax原理
封装ajax原理 首先处理 用户如果不传某些参数,设置默认值 type默认get 默认url为当前页 默认async方式请求 data数据默认为{} 处理用户传进来的参数对象 遍历,拼接成key=va ...