两年未写总结博客,今天先来练练手,总结最近遇到的一个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://stackoverflow.com/questions/41488093/why-do-i-get-runtime-error-when-comparison-function-in-stdsort-always-return-t

  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的分析过程的更多相关文章

  1. KMP算法的next函数求解和分析过程

    转自 wang0606120221:http://blog.csdn.net/wang0606120221/article/details/7402688 假设KMP算法中的模式串为P,主串为S,那么 ...

  2. ptyhon 编程基础之函数篇(二)-----返回函数,自定义排序函数,闭包,匿名函数

    一.自定义排序函数 在Python中可以使用内置函数sorted(list)进行排序: 结果如下图所示: 但sorted也是一个高阶函数,可以接受两个参数来实现自定义排序函数,第一个参数为要排序的集合 ...

  3. 冒泡排序和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] ...

  4. PHP常用数字函数以及排序函数

    一:数字函数 .ceil() 进一取整 示例:ceil(0.9) 结果为1 .abs() 绝对值 示例:abs(-1) 结果为1 .rand() 随机数 示例:rand(1. 100) 1到100 以 ...

  5. c++中std::set自定义去重和排序函数

    c++中的std::set,是基于红黑树的平衡二叉树的数据结构实现的一种容器,因为其中所包含的元素的值是唯一的,因此主要用于去重和排序.这篇文章的目的在于探讨和分享如何正确使用std::set实现去重 ...

  6. std list/vector sort 自定义类的排序就是这么简单

    所以,自己研究了一下,如下:三种方式都可以,如重写<,()和写比较函数compare_index.但是要注意对象和对象指针的排序区别. 1.容器中是对象时,用操作符<或者比较函数,比较函数 ...

  7. (C++)STL排序函数sort和qsort的用法与区别

    主要内容: 1.qsort的用法 2.sort的用法 3.qsort和sort的区别 qsort的用法: 原 型: void qsort(void *base, int nelem, int widt ...

  8. C++ 中的sort()排序函数用法

    sort(first_pointer,first_pointer+n,cmp) 该函数可以给数组,或者链表list.向量排序. 实现原理:sort并不是简单的快速排序,它对普通的快速排序进行了优化,此 ...

  9. hdu 1263 水果 结构的排序+sort自定义排序

    水果 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submissi ...

随机推荐

  1. oracle中nvarchar2查询结果显示总是少一位

    问题: 有个表的字段是nvarchar2(32),但是在plsql中查询显示结果发现一直少一位. 修改方法: 在plsql里的首选项-连接里有个选项: 在oci8上强制使用oci7, 把这个勾上就ok ...

  2. 给JS包写TypeScript用的类型申明文件

    TS (TypeScript)区别于JS (JavaScript)一个最大的不同是TS增加了类型.当一些TS代码要使用JS包的时候,最好这些JS包都有类型介绍,比如这个变量是什么类型,那个函数参数的什 ...

  3. 我的Python笔记补充:入门知识拾遗

    声明:本文整理借鉴金角大王的Python之路,Day1 - Python基础1,仅供本人学习使用!!! 入门知识拾遗 一.bytes类型 二.三元运算 1 result = 值1 if 条件 else ...

  4. 使用pl/sql developer登陆不了oracle

    1,Oracle ORA12514 监听程序当前无法识别连接描述符中请求的服务 这里最主要的原因在于:(参考:https://www.cnblogs.com/shangshan/p/6359880.h ...

  5. resume

    源码链接(码云):https://gitee.com/tinqiao/level_17_software_engineering.git 截图效果: 源码: <!DOCTYPE html> ...

  6. Windows 运行命令大全,装逼必备哦!

    以下已整理,以字母先后排序: appwiz.cpl:程序和功能 cliconfg:SQL SERVER 客户端网络实用工具 cmd:CMD命令提示符 comexp.msc或者dcomcnfg:组件服务 ...

  7. MongoDB与SpringBoot整合(支持事务)

    1.创建SpringBoot工程,选择 Web.MonogDB 依赖,pom如下: <parent> <groupId>org.springframework.boot< ...

  8. OO Unit 1 表达式求导

    OO Unit 1 表达式求导 面向对象学习小结 前言 本博主要内容目录: 基于度量来分析⾃己的程序结构 缺点反思 重构想法 关于BUG 自己程序出现过的BUG 分析⾃己发现别人程序bug所采⽤的策略 ...

  9. time-based基于google key生成6位验证码(google authenticator)

    由于公司服务器启用了双因子认证,登录时需要再次输入谷歌身份验证器生成的验证码.而生成验证码是基于固定的算法的,以当前时间为基础,基于每个人的google key去生成一个6位的验证码.也就是说,只要是 ...

  10. 生产环境,vue页面跳转的时候,js报404的问题

    最近上线的一个vue项目,需要各种路由跳转,在开发和测试环境都没问题,但是在生产环境,发现后期更新代码的时候,有些机型(ios机型,暂未发现android有问题)跳转路由的时候,标题修改了,但是内容并 ...