【问题标题】:Is there an efficient way to use views::filter after transform? (range adaptors) [duplicate]转换后是否有一种有效的方法来使用views::filter? (范围适配器)[重复]
【发布时间】:2021-11-14 19:12:53
【问题描述】:

views::filter 异常行为的常见示例:

#include <iostream>
#include <ranges>
#include <vector>

int main ()
{
    using namespace std;

    auto ml = [](char c) // ml = make lambda (always accepts / transforms to 1)
    {
        return [c](int) {cout << c; return 1;};
    };

    vector<int> vec = {1};
    
    auto view = vec
        | views::transform  (ml('T'))
        | views::filter     (ml('F'));
    
    // use view somehow:
    return *view.begin();
}

输出TFT(注意额外的T)。 demo


我们必须知道:

auto view = vec
    | views::transform  (ml('A'))
    | views::filter     (ml('B'));

...只是语法糖:

auto view = views::filter(views::transform(vec, ml('A')), ml('B'));

问题说明:

已经实现了views::filter 中的几个mock versions,问题似乎是:

  • 与其他迭代器不同,filter::iteratoroperator ++ 期间工作(搜索可接受的值)
  • operator * 设计用于提取值,但使用 filter::iterator,工作已经完成并丢失(我们不需要重新搜索可接受的值,但我们确实需要重新计算它)
  • 我们无法存储结果,因为视图的constant-time copy constraint(值可能是一个数组)

为了在图片中解释这一点,我们将表示迭代的过程:

view = container | Transform | Filter1 | Filter2 | Filter3

(为丑陋的图表道歉)圆圈代表Pr(F3) 正在为Print F3 完成的工作(模拟示例中完成的工作):

我们可以看到,如果我们只组合过滤器或只组合变换,那么我们就不会重复工作 - 问题在于变换上方有过滤器(在图表上方)。

在最坏的情况下,我们得到[n(x)x 的数量]:
n(iterations) = n(filters) * n(transforms)
当我们期望n(filters) + n(transforms)

我们可以通过this example 看到这种抛物线与线性增长。


问题

有时需要先完成transforms,然后才能确定filter,那么如何避免额外的工作量?这似乎很重要,因为 views 旨在迭代容器,这是瓶颈领域。

有没有办法在这种情况下使用std::views 而不会出现上述巨大的减速?

【问题讨论】:

    标签: c++ gcc g++ c++20 std-ranges


    【解决方案1】:

    我认为考虑这一点的一个好方法是过滤器需要获取其输入的值才能决定是否执行过滤。然后当你评估过滤器的输出时,我们必须再次评估,除非过滤器已经缓存了输入(见下文)。

    从交替过滤器和视图看来,每个过滤器都会构建其下方所有转换的列表,然后使用此转换列表来评估结果。所以修改你的例子

    #include <iostream>
    #include <ranges>
    #include <vector>
    
    int main()
    {
    
        auto ml = [](char c) // ml = make lambda (always accepts / transforms to 1)
        {
            return [c](int) {std::cout << c; return 1; };
        };
    
        std::vector<int> vec = { 1,1,1 };
        auto view = vec
            | std::views::transform(ml('T'))
            | std::views::filter(ml('F'))
            | std::views::transform(ml('U'))
            | std::views::filter(ml('G'));
    
        for( auto v : view)
            std::cout << v << ",";
    
        return 0;
    }
    

    给出输出

    TFTUGTU1,TFTUGTU1,TFTUGTU1,

    我们想要获取我们的输入,然后进行变换 T,过滤 F,变换 U,过滤 G。所以我们可能期望输出 TFUG1。然而,这似乎是发生了什么

    过滤器 G 需要知道值是否已经被过滤,并且需要知道元素值以知道它是否应该自己进行任何过滤。所以它把它传递到链上的下一个过滤器 - F。

    过滤器 F 需要知道它是否需要过滤,因此它评估变换 T,然后进行过滤并发现它可以传递值,所以它将 T 添加到它的变换列表中。

    G 现在可以决定是否需要过滤。它使用 F 的变换列表(仅包括 T)和 F 和 G 之间的任何变换来评估输入。所以我们称变换 T 和 U。

    G 现在进行过滤并发现它可以传递该值。所以它将下面的所有变换添加到它的变换列表中,这个列表现在是 T 和 U。

    最后我们想要这个值,所以 G 通过调用它的转换列表 T 和 U 来评估所有内容。我们现在有了我们的结果。

    请注意,这是来自观察的概念模型。我没有搜索标准或代码来检查这一点,这只是示例中的显示方式。

    您可以将其视为一棵树

    G
    |\
    U U
    |  \
    F   \
    |\   \
    T T   T
    

    每个过滤器都会导致一个分支,左侧带有过滤器和变换,而右侧只变换。我们评估每个过滤器节点的左侧,然后是过滤器节点本身,然后是每个过滤器节点的右侧分支。

    复杂度如你所说,O((n_filters+1)*n_transforms)。

    我的怀疑是,如果您提供了一个 constexpr 作为转换,那么编译器可以将其优化为 TFUG 的简单线性评估,因为它会看到每个评估的返回值都是相同的。但是,在转换中添加 std::cout 的行为意味着这不是 constexpr,因此这些优化不会发生。

    因此我怀疑这里发生的事情是通过尝试显示 C++ 代码的结构,您已经停止了二进制代码中发生的优化。

    您当然可以通过创建一个 constexpr 转换并检查运行时发生的情况来测试这一点,因为您在启用优化的情况下增加了视图层。

    编辑


    我最初说我认为过滤器正在缓存一个转换列表,如果他们这样做,那么如果转换是 constexpr 那么他们可能会缓存这些传输的结果。经过反思和更多阅读,我认为过滤器根本没有缓存转换列表。他们在递增迭代器时评估每个分支的左侧,在取消引用时评估右侧。

    但是,我仍然认为使用 constexpr 转换,编译器可以有效地优化它。在不检查代码的情况下,也许过滤器可以进行缓存——我相信这里有人知道。所以我认为测试一个 constexpr 优化的例子还是有用的。

    【讨论】:

    • 感谢您的回复。缓存当然可以在第二次迭代中发挥作用(我们可以通过从我的问题中删除最后一个 code example 中的 for 循环来看到这一点:副本从 27 次打印到 36 次)。我不确定constexpr 与一般问题有多相关。树的东西或多或少是我试图在我凌乱的图表中解释的东西。在我做了一些模拟实现之前,我个人完全不知道它是如何工作的——那么这不仅仅是观察。我不太相信这是对 c++ 的一个很好的补充。我们拭目以待。
    • @Elliott 我想我应该说pure 函数而不是constexpr。或者constexpr 是正确的术语。无论哪种方式,因为转换输出到 std::cout,所以必须调用它。如果它没有进行该调用,那么编译器可能会优化掉重复调用。使用constexpr 将向编译器暗示这可能与编译器特定的pure 标志一样。 gcc 和 clang 对此有 __attribute__((pure)),但它不是标准化的。
    猜你喜欢
    • 2023-01-23
    • 2020-01-14
    • 2017-06-24
    • 2021-10-06
    • 2017-03-28
    • 2019-05-13
    • 1970-01-01
    • 2013-09-14
    • 1970-01-01
    相关资源
    最近更新 更多