この記事について質問がある場合は、公式アカウント [高性能アーキテクチャの探索] にメッセージまたはプライベート メッセージを残すか、著者の WeChat を追加して直接通信することができます。さらに、無料のコンピューターのバッチがあります。電子書籍、およびバックグラウンド返信 [pdf] は無料で入手できます。
皆さんこんにちは、ユルです!
しばらく前に、同僚が私のところに来て、プログラムのコアダンプが厳密な弱い順序付けによって引き起こされ、会社に数百万の損失をもたらしたと言いました.最終的な格付け違反はP0で、年末のボーナスは少し失われました. . これを聞いた後、思わずため息が出ました。
前回の記事でstd::sort_のソースコード実装を分析しましたが、データ量が多い場合は高速ソートと分割再帰ソートを使います。セグメント化されたデータの量が特定のしきい値を下回ると、クイック ソートの再帰呼び出しによって発生する追加のオーバーヘッドを回避するために、この時点で挿入ソートが使用されます。再帰レベルが深すぎる場合は、ヒープ ソートも使用されます。_
今日、この記事の助けを借りて、この失敗の原因を分析し、その後の開発プロセスで同様の問題を回避します。
バックグラウンド
トラフィックがリコールやフィルタリングなどの一連の操作を通過した後、最終的な広告候補セットが取得されます。これは、対応する戦略に従ってソートする必要があり、最終的に最初の最適な広告が返されます。
struct AdItem {
std::string ad_id;
int priority;
int score;
};
並べ替える必要がある AdItem タイプのバークターがあり、並べ替えのルールは次のとおりです。
-
優先度の高い順に並べ替え
-
優先度が同じ場合はスコアの降順でソートされます
要件は比較的単純で、当時のオンライン コードは次のとおりです。
void AdSort(std::vector<AdItem> &ad_items) {
std::sort(ad_items.begin(), ad_items.end(), [](const AdItem &item1, const AdItem &item2) {
if (item1.priority < item2.priority) {
return true;
} else if (item1.priority > item2.priority) {
return false;
}
return item1.score >= item2.score;
} );
}
テスト環境は、テスト ケースを構築し、期待を満たし、オンラインになります。
恐ろしいことに、起動してすぐにプログラムが直接コアダンプし、その後自動的に再起動し、その後コアダンプが発生した、と当時は感じていました。
位置
まず、オンライン サーバーにログインし、gdb を介してスタック情報を表示します。
オンライン版はリリース版なのでスタック情報が読めないので、デバッグ版にコンパイルして、ある行をグレースケールにしてみましたが、やはりクラッシュしてしまい、スタック情報を確認しました。
スタック情報によるとAdSort関数を実行した直後にクラッシュが発生し、std::vectorが破壊されていたが、今回の起動が原因と思われるため、コードをロールバックして原因を再解析した。
理由
できるだけ早く原因を突き止めるために、このコードとオンライン ベクトル値を取得し、ローカルで小規模なテストをビルドします。基本的なコードは次のとおりです。
oid AdSort(std::vector<AdItem> &ad_items) {
std::sort(ad_items.begin(), ad_items.end(), [](const AdItem &item1, const AdItem &item2) {
if (item1.priority < item2.priority) {
return true;
} else if (item1.priority > item2.priority) {
return false;
}
return item1.score >= item2.score;
} );
}
int main() {
std::vector<AdItem> v;
/*
给v进行赋值操作
*/
AdSort(v);
return 0;
}
次のコマンドを実行して、コンパイルして実行します。
g++ -g test.cc -o test
./test
次のようにエラーを実行しています。
gdb を使用してスタック情報を表示する
オンラインの問題が再発し、コアダンプが AdSort によるものであることが基本的に確認されました.しかし、AdSort では単純な並べ替えでは、並べ替えがクラッシュすることはありません.唯一の理由は、記述されたラムダ関数に問題があるためです.
_step by step 位置付けと除外方法_ を使用して、ラムダ関数を再変更し、実行して、正常に実行します。
void AdSort(std::vector<AdItem> &ad_items) {
std::sort(ad_items.begin(), ad_items.end(), [](const AdItem &item1, const AdItem &item2) {
if (item1.priority < item2.priority) {
return true;
} else if (item1.priority > item2.priority) {
return false;
}
if (item1.score > item2.score) {
return true;
}
return false;
} );
}
正常に動いているのは、ラムダ比較機能に問題があるためなので、なぜ大丈夫なのですか?
<> before_Article 21: 等しい場合は常に比較関数が false を返すようにするという文を見たことを思い出しました。この原則に従わないことが原因でコアダンプが発生するはずです。
では、なぜこの原則に従うのでしょうか。Google を開いて std::sort coredump と入力し、文を確認します。
非循環関係を持つことは、
<
オペレーターにとって非推移性と呼ばれます。人間関係が循環している場合、妥当な結果が得られないことを理解するのはそれほど難しくありません。実際、C++ STL アルゴリズムから正しい結果を得るために、データ型とそのコンパレーターが従わなければならない一連の非常に厳密な規則があります。これは厳密な弱い順序付けです。
上記の意味から、STL では、sort 関数のソートアルゴリズムは厳密な弱い順序付けの原則に従う必要があります。
厳密な弱い順序
厳密な弱い順序付けとは何ですか? ウィキペディアの定義からの抜粋:
厳密な弱順序付けは、集合S上の二項関係<であり、厳密な半順序(非反射的である推移関係、または同等の [ 5]非対称である)であり、その関係は「a < bでもb < aでもありません」 [ 1]したがって、厳密な弱い順序付けには次のプロパティがあります。
- Sのすべてのxについて、 x < x ( irreflexivity )とは限りません。
- Sのすべてのx、yについて、x < yの場合、 y < x (非対称)ではありません。
- Sのすべてのx、y、zについて、x < yおよびy < zの場合、 x < z (推移性) です。
- Sのすべてのx、y、zについて、xがyと比較不能であり( x < yでもy < xでもない)、yがzと比較不能である場合、xはzと比較不能です(比較不能の推移性)。
上記の概念を要約すると、x と y の 2 つの変数があるということです。
- x > y は y < x と同等です
- x == y は !(x < y) && !(x > y) と同等です
厳密に弱い順序にしたい場合は、次の規則に従う必要があります。
- すべての x について: x < x が真になることはありません。各変数の値はそれ自体と等しくなければなりません。
- x < y の場合、y < x は真ではありません
- x < y かつ y < z の場合、x < z、つまり順序付けは推移的でなければなりません
- x == y かつ y == z の場合、x == z となります。つまり、同じ値も推移的でなければなりません。
では、なぜ厳密な弱い順序のルールに従わないと、コアダンプが発生するのでしょうか?
の
std::sort()
場合、コンテナ内の要素数が_S_threshold
列挙定数値より大きい場合、クイック ソートが使用されます。
まず、並べ替えの関数呼び出しチェーンを見てみましょう (コアダンプを引き起こさない部分は削除されています)。
sort
-> __introsort_loop
--> __unguarded_partition
__unguarded_partition 関数の定義を見てみましょう。
template<typename _RandomAccessIterator, typename _Tp, typename _Compare>
_RandomAccessIterator
__unguarded_partition(_RandomAccessIterator __first,
_RandomAccessIterator __last,
_Tp __pivot, _Compare __comp)
{
while (true)
{
while (__comp(*__first, __pivot))
++__first;
--__last;
while (__comp(__pivot, *__last))
--__last;
if (!(__first < __last))
return __first;
std::iter_swap(__first, __last);
++__first;
}
}
上記のコードには、次の段落があります。
while (__comp(*__first, __pivot))
++__first;
その中で、first は反復子、pivot は中間値、comp は受信比較関数です。
入力ベクトル内の後続の要素が完全に等しい場合、comp 比較関数は常に true であり、後続の ++__first は最終的に反復子を無効にし、コアダンプが発生します。
さて、ここまでで、オンライン障害の原因の分析は完了しました。
エピローグ
正直なところ、この失敗については何も言うことはありません. 経験の浅い学習と説得力のせいで自分を責めることしかできません. それは自分自身への教訓と見なすことができます. 今後のテストケースは、ラインとできるだけ一致しています.問題はテスト段階でできるだけ早く明らかになります。
今回は、この失敗の原因を共有し、その後の開発プロセスで誰もがマイニング ピットを回避できることを願っています。
では、今回の記事は以上です。また次回お会いしましょう。