问题 std :: remove和std :: remove_if设计的稳定性是否失败?


最近(从一篇SO评论中)我了解到了这一点 std::remove 和 std:remove_if 很稳定我错误地认为这是一个糟糕的设计选择,因为它阻止了某些优化?

想象一下,删除1M的第一个和第五个元素 std::vector。由于稳定性,我们无法实施 remove 与交换。相反,我们必须改变所有剩余的元素:(

如果我们不受稳定性的限制,我们可以(对于RA和BD iter)实际上有2个iters,一个从前面,第二个从后面,然后使用swap来将待移除的项目结束。我相信聪明的人可能会做得更好。我的问题一般,而不是我正在谈论的具体优化。

编辑: 请注意,C ++广告零开销原则,也有 std::sort 和 std::stable_sort 排序算法。

EDIT2:  优化将类似于以下内容:

对于 remove_if

  • bad_iter从头开始查找谓词返回true的那些元素。
  • good_iter从最后查看谓词返回false的元素。

当两者都找到了预期时,他​​们就会交换他们的元素。终止是在 good_iter <= bad_iter

如果它有帮助,可以把它想象成快速排序算法中的一个,但是我们不将它们与特殊元素进行比较,而是使用上面的谓词。

EDIT3: 我玩了一遍,试图找到最坏的情况(最糟糕的情况是 remove_if  - 注意谓词很少是真的)我得到了这个:

#include <vector>
#include <string>
#include <iostream>
#include <map>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <memory>
using namespace std;
int main()
{  
    vector<string> vsp;
    int n;
    cin >> n;
    for (int i =0; i < n; ++i)
    {   string s = "123456";
        s.push_back('a' + (rand() %26));
        vsp.push_back(s);
    }
    auto vsp2 = vsp;
    auto remove_start = std::chrono::high_resolution_clock::now();
    auto it=remove_if(begin(vsp),end(vsp), [](const string& s){ return s < "123456b";});
    vsp.erase(it,vsp.end());
    cout << vsp.size() << endl;
    auto remove_end = std::chrono::high_resolution_clock::now();
    cout << "erase-remove: " << chrono::duration_cast<std::chrono::milliseconds>(remove_end-remove_start).count() << " milliseconds\n";

    auto partition_start = std::chrono::high_resolution_clock::now();
    auto it2=partition(begin(vsp2),end(vsp2), [](const string& s){ return s >= "123456b";});
    vsp2.erase(it2,vsp2.end());
    cout << vsp2.size() << endl;
    auto partition_end = std::chrono::high_resolution_clock::now();
    cout << "partition-remove: " << chrono::duration_cast<std::chrono::milliseconds>(partition_end-partition_start).count() << " milliseconds\n";
}



C:\STL\MinGW>g++ test_int.cpp -O2 && a.exe
12345678
11870995
erase-remove: 1426 milliseconds
11870995
partition-remove: 658 milliseconds

对于其他用法,分区更快,相同或更慢。让我困惑的颜色。 :d


6164
2017-12-11 10:33


起源

我不认为这是一个失败。软件设计完全取决于权衡。如果标准算法不能满足您的某些要求,那么就没有什么能阻止您推出自己的算法。 - NPE
“由于稳定性,我们无法使用swap实现删除,而是必须移动每个剩余的元素“。如果向量被排序,并且交换违反了那个不变量怎么办?另外,如果你需要这样的优化,你可以根据你的软件和设计的要求,总是实现你自己的删除。 - Nawaz
@NoSenseEtAl:或者你可以调用你的算法 unordered_remove() 或类似的。我认为您提出的算法足以包含在标准库中,但现有算法也是如此。哪个人应该得到“规范”名称的问题 remove() 是政治的。 - j_random_hacker
@NoSenseEtAl:您在第二次编辑中的提案已经在标准中,它被称为 std::partition。来自POV remove_if,它不必要地保留每个值的一个副本,并且由于这个原因效率低下。但你可以调整它从好元素转移到坏元素,而不是交换。 - Steve Jessop
这为什么关闭?他没有哭 “哇哇,我希望我不稳定地从邪恶的标准委员会中删除” 但问一个关于C ++标准库设计的有效问题。即使你不同意他的推理(这就是答案的答案),我也不明白为什么这不是建设性的。 - Christian Rau


答案:


我假设你在问一个假设的定义 stable_remove 是什么 remove 目前是,和 remove 然而,实施者认为最好以任何顺序给出正确的值。期望实施者能够在完全相同的情况下进行改进 stable_remove

在实践中,图书馆不能 容易 做这个优化。这取决于数据,但您不想花太多时间来确定在决定如何删除每个元素之前将删除多少元素。例如,你可以做一个额外的传递来计算它们,但是有很多情况下额外传递是低效的。仅仅因为在某些情况下不稳定的移除比稳定更快并不一定意味着在两者之间进行选择的自适应算法是一个不错的选择。

我认为之间的区别 remove 和 sort 是排序是 已知 是一个复杂的问题,有很多不同的解决方案和权衡和调整。所有“简单”排序算法都很慢 一般。大多数标准算法非常简单,而且 remove 是其中之一但是 sort 不是。因此,我认为定义并不是很有意义 stable_remove 和 remove 作为单独的标准功能。

编辑:使用我的调整进行编辑(类似于 std::partition但是没有必要保持右边的值)对我来说似乎很合理。它需要一个双向迭代器,但在标准中有一些先例可用于在不同的迭代器类别上表现不同的算法,例如 std::distance。因此标准可以定义 unstable_remove 只有 要求 一个前向迭代器,但是如果它得到一个bidi迭代器就做你的事情。标准可能不会列出算法,但它可能有一个短语,如“如果迭代器是双向的,最多会 min(k, n-k) 移动到哪里 k 是删除的元素的数量“,这实际上会强制它。但请注意,标准目前没有说明有多少动作 remove_if 是的,所以我认为固定下来并不是一个优先事项。

当然没有什么能阻止你实现自己的 unstable_remove

如果我们接受标准不需要指定不稳定的删除,则问题归结为它是否应该调用它所定义的函数 stable_remove,期待未来 remove 对于bidi迭代器来说行为不同,并且对于前向迭代器可能表现不同,如果用于执行不稳定删除的一些聪明的启发式变得已经足够已知值得标准函数。我不这样说:如果标准功能的名称不完全正常,那就不是灾难。从STL中删除稳定性的保证可能是非常具有破坏性的 remove_if。然后问题就变成了,“为什么STL没有这样称呼它 stable_remove_if“除了所有答案中的所有要点之外,我只能回答这个问题,STL设计过程比标准化过程更快。

stable_remove 也会打开一些关于其他标准功能的蠕虫病毒 理论上 有不稳定的版本。对于一个特别愚蠢的例子应该 copy 叫做 stable_copy,以防一些实现存在,它在复制时明显更快地反转元素的顺序?应该 copy 叫做 copy_forward,这样实现可以选择哪个 copy_backward 和 copy_forward 被称为 copy 根据哪个更快?委员会的部分工作是在某处画一条线。

我认为现实的标准是明智的,单独定义一个是明智的 stable_remove 和a remove_with_some_other_constraints但是 remove_in_some_unspecified_way 只是没有提供相同的优化机会 sort_in_some_unspecified_way 确实。 Introsort是在1997年发明的,就像C ++正在标准化一样,但我不认为这是一项研究工作 remove 是它的本来就是这样 sort。我可能错了,优化 remove 可能是下一件大事,如果是这样,那么委员会就错过了一招。


12
2017-12-11 11:15



你为什么需要额外的通行证?我认为他的提议,当你必须删除时,实际上是做类似的事情 swap(*it_to_remove,*(last_it--));。 - KillianDS
@KillianDS:如果你无条件地这样做,那么你的算法比实际慢 remove 一般。例如,当移除一半元素时,它移动的次数是移动的三倍。所以你不能只是实施 remove_if 要做到这一点作为图书馆的“优化”,那将是垃圾。我正在考虑这个案子 stable_remove_if 是什么 remove_if 目前是,和 remove_if 是实施者的选择如何优化。如果这不是提问者的问题那么我可以再试一次:-) - Steve Jessop
哦,我当然同意这一点(这基本上就是我自己回答的)。我只是误解了额外的传球错误。 - KillianDS
@SteveJessop看到我的第二次编辑。 - NoSenseEtAl
啊,所以对于稀疏删除std :: partition是要走的路......:D - NoSenseEtAl


std::remove 指定与前向迭代器一起使用。

从开始到结束使用一对迭代器的方法会增加迭代器的要求,从而降低函数的效用或违反/恶化渐近复杂度保证。


3
2018-04-27 19:49



这是一个好点。它的优雅程度如何 stable_remove 采取前进迭代器和 unstable_remove 采取双向迭代器?委员会是否有足够的理由以这些理由驳回它?我不认为提问者是说现有的 remove 不应该在标准中,只是不应该被调用 remove 因为这个名字没有宣传你(他认为)支付稳定性。 - Steve Jessop
@NoSenseEtAl:好吧,memcpy只有在迭代器类型是指向POD的指针时才有效(在C ++ 11中,需求变为指向可以复制的指针)。或者,例如,优化器可以确定迭代器的行为就像它一样 vector::iterator 可能是指针的薄包装器。 deque<shared_ptr<string>> 有随机访问迭代器,但你没有太多运气复制它 memcpy 有两个原因(deque 不连续,你需要调用赋值运算符来增加refcount)。 - Steve Jessop
@SteveJessop是的,我曾经用deque做过那个确切的错误:D我不记得ms使用memcpy究竟是什么,但我记得它在某个地方使用过......无论哪种方式都不适用于RA范围的副本。我错了 - NoSenseEtAl
在A评论:它错过了重点。有人提到std :: distance() - 同名,显然是FW和它的差异实现。 - NoSenseEtAl


回答我自己的问题> 3年后:)
是的,这是一个“失败”。

有一个提案 D0041R0 这将添加unstable_remove。 有人可能会说,只是因为有一个建议添加std :: unstable_remove,这并不意味着std :: remove是一个错误,但我不同意。 :)


1



如果您要链接到提案,则应链接到 实际提案,而不是它的旧稿。至于默认值是否应该是“不稳定”形式,我认为基本上每个想要从某些东西中删除元素的人都会感到惊讶。能够获得更高的表现 remove 是 重要;别误会我的意思。但要捍卫它应该是的理念要困难得多 默认,当行为会出乎意料。 - Nicol Bolas