为什么GCC -O3在std::deque上使用过滤器迭代器会导致std::距离无限?



在经历了许多痛苦和痛苦之后,我发现了一些非常奇怪的行为,当给定boost::filter_iterator s超过std::deque的范围时,std::distance永远不会返回。似乎这个问题是GCC(6.1+)与-O3优化所特有的。下面是演示违规行为的示例:

#include <string>
#include <deque>
#include <iterator>
#include <iostream>
#include <boost/iterator/filter_iterator.hpp>
struct Foo
{
    std::string bar, s = "";
    char a = '';
};
int main()
{
    const std::deque<Foo> foos(14, {""});
    const std::string test {};
    const auto p = [test] (const auto& foo) { return foo.bar == test; };
    using boost::make_filter_iterator;
    const auto begin = make_filter_iterator(p, std::cbegin(foos), std::cend(foos));
    const auto end   = make_filter_iterator(p, std::cend(foos), std::cend(foos));
    std::cout << std::distance(begin, end) << std::endl;
}

一些观察:

  • GCC与优化-O2或更少的回报预期。
  • Clang(3.8)返回任何优化级别的正确答案。
  • std::deque更改为std::vectorstd::list将导致预期的行为。
  • 14是关键;
  • sizeof(Foo)很重要;删除sa使问题消失。
  • 通过引用捕获test,或者只是比较常量表达式(例如foo.bar == " ")会导致正常的行为。
  • 没有编译器警告(-Wall -Wextra -pedantic)。
  • Valgrind报告没有错误。
  • 使用fsanitize=undefined,问题就解决了。

怎么回事?

我认为下面的这些发现可能对改进bug报告和在代码中使用来解决问题都很有用。

通过调试优化后的输出和使用优化标志以及对代码进行微小的修改,我已经得出了导致错误的特定优化标志的结论。

选项集如下:

-O -fno-auto-inc-dec -fno-branch-count-reg -fno-combine-stack-adjustments -fno-compare-elim -fno-cprop-registers -fno-dce -fno-defer-pop -fno-delayed-branch -fno-dse -fno-forward-propagate -fno-guess-branch-probability -fno-if-conversion2 -fno-if-conversion -fno-inline-functions-called-once -fno-ipa-pure-const -fno-ipa-profile -fno-ipa-reference -fno-merge-constants -fno-move-loop-invariants -fno-reorder-blocks -fno-shrink-wrap -fno-split-wide-types -fno-ssa-backprop -fno-ssa-phiopt -fno-tree-bit-ccp -fno-tree-ccp -fno-tree-ch -fno-tree-coalesce-vars -fno-tree-phiprop -fno-tree-sink -fno-tree-slsr -fno-tree-dse -fno-tree-forwprop -fno-tree-fre -fno-unit-at-a-time -fno-tree-ter -fno-tree-sra -fno-tree-copy-prop -fstrict-aliasing -ftree-slp-vectorize -std=c++14

很抱歉这么长时间,但我真正想要的是:-O0 -ftree-copy-prop -ftree-pta -ftree-dce -fstrict-aliasing -ftree-slp-vectorize(我也尝试过-Og)加上01的神奇步骤…

请注意,只有-O3 -f-no-tree-slp-vectorize将已经修复的行为,但通过使用完整的选项,我已经发送的调试几乎很容易…

而且,操作符==(string, string)的存在似乎在编译器中产生了混淆。

如果您检查粘贴在下面的代码,其中所有由# If 0注释的代码,当激活工作在原始代码的地方时,您可能会发现我没有发现的问题。

注意,==()运算符甚至没有被调用,因为在测试中foo.a != ''总是为真。因此,看起来它的存在使编译器生成了不好的代码。

还要注意,循环中的任何注释代码也会将行为更改为预期的行为,这就是为什么我怀疑初学者有向量化标志的原因。

#include <string>
#include <deque>
#include <iterator>
#include <iostream>
#include <boost/iterator/filter_iterator.hpp>
#include <string.h>
struct Foo
{
    std::string bar, s = "";
    char a = 'n';
};
std::ostream& operator<<(std::ostream& os, const Foo& f)
{
    os << f.bar << '/' << f.a;
    return os;
}
int main()
{
    std::deque<Foo> foos(14, {"abc"});
    const std::string test {"abc"};
    Foo other;
    other.bar = "last"; other.a = 'l';
    foos.push_back(other);
    other.bar = "first";
    other.a = 'f';
    foos.push_front(other);
    //
#if 0
    const auto p = [test] (const auto& foo) { return foo.a != ''; };
#elif 0
    const auto p = [test] (const auto& foo) {
        bool  rc =  (foo.a != '');
        if (!rc)
            rc = (foo.bar == std::string(test));
        return rc;
    };
#elif 1
    const auto p = [test] (const auto& foo) {
        bool  rc =  (foo.a != '');
        if (!rc)
            rc = (foo.bar == test);
        return rc;
    };
#endif
    using boost::make_filter_iterator;
    const auto begin = make_filter_iterator(p, std::cbegin(foos), std::cend(foos));
    const auto end   = make_filter_iterator(p, std::cend(foos), std::cend(foos));
    std::cout << std::distance(end, end) << std::endl;
    std::cout << std::distance(begin, begin) << std::endl;
    std::cout << std::distance(std::cbegin(foos), std::cend(foos)) << std::endl;
    auto __first = begin;
    auto __last = end;
    int __n = 0;
    //std::cout << __last << std::endl;
    //std::deque<char> trace;
    //Foo trace[21];
    const int max = foos.size();
    char trace[max+5]; memset(trace, 'c', sizeof(trace));
    std::cout << max << std::endl;
    std::cout << *__last << std::endl;
    while (__first != __last)
    {
        trace[__n] = (*__first).a;
        //trace[__n] = (*__first);
        //trace.push_back((*__first).a);
        //std::cout << *__first << std::endl;
        ++__n;
        ++__first;
        if (__n > max + 5)
            break;
        //std::cout << __n << std::endl;
        //std::cout << (__first != __last) << std::endl;
    }
    for (auto f: trace)
        std::cout << f  << std::endl;
    std::cout << "Tadaaaaa: " <<  __n << std::endl;
    //std::cout << std::distance(begin, end) << std::endl;
}

这个行为是由于一个由糟糕的向量化优化引起的GCC错误。现在已经发布了一个修复,应该会出现在GCC 6.3中。

对于那些使用GCC 5.4 - 6.2的用户,编译器选项-fno-tree-slp-vectorize将会"修复"这个问题。

最新更新