【问题标题】:Is stability of std::remove and std::remove_if design fail?std::remove 和 std::remove_if 设计的稳定性是否失败?
【发布时间】:2012-12-11 10:33:01
【问题描述】:

最近(来自一个 SO 评论)我了解到 std::removestd:remove_if 是稳定的。我是否错误地认为这是一个糟糕的设计选择,因为它会阻止某些优化?

想象一下删除 1M std::vector 的第一个和第五个元素。因为稳定性,我们不能用交换实现remove。相反,我们必须移动所有剩余的元素。 :(

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

编辑:请注意,C++ 宣传零开销原则,还有std::sortstd::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

【问题讨论】:

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

标签: c++ stl complexity-theory


【解决方案1】:

我假设您是在询问 stable_remove 的假设定义,即 remove 当前是什么,remove 要实现,但是实现者认为最好以任何顺序给出正确的值。期望实现者能够改进与stable_remove 完全相同的操作。

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

我认为removesort 之间的区别在于,众所周知排序是一个复杂的问题,需要许多不同的解决方案以及权衡和调整。所有“简单”的排序算法平均而言都很慢。大多数标准算法都非常简单,remove 是其中之一,但sort 不是。我认为将stable_removeremove 定义为单独的标准函数没有多大意义。

编辑:您使用我的调整进行的编辑(类似于std::partition,但无需保留右侧的值)对我来说似乎很合理。它需要一个双向迭代器,但标准中有先例,算法在不同的迭代器类别上表现不同,例如std::distance。因此,标准可以定义 unstable_remove 需要 一个前向迭代器,但如果它得到一个双向迭代器,它就会做你的事情。该标准可能不会列出算法,但它可能有一个短语,例如“如果迭代器是双向的,则最多 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_backwardcopy_forward 中的哪一个被copy 调用,根据哪个更快?委员会的部分工作是在某处划清界限。

我认为实际上当前的标准是明智的,分开定义stable_removeremove_with_some_other_constraints 是明智的,但remove_in_some_unspecified_way 只是没有提供与sort_in_some_unspecified_way 相同的优化机会。 Introsort 是在 1997 年发明的,就像 C++ 正在标准化一样,但我不认为围绕 remove 的研究工作与现在和围绕 sort 的研究工作完全一样。我可能错了,优化remove 可能是下一件大事,如果是这样,那么委员会就错过了一个窍门。

【讨论】:

  • 为什么需要额外的通行证?我认为他的建议,当你必须删除时,实际上是做类似swap(*it_to_remove,*(last_it--)); 的事情。
  • @KillianDS:如果你无条件地这样做,那么你的算法平均比真正的remove 慢。例如,当删除一半元素时,它会执行三倍的移动。所以你不能仅仅实现remove_if 来作为库“优化”来做到这一点,这将是垃圾。我正在考虑stable_remove_ifremove_if 当前是什么情况,remove_if 是实现者选择如何优化的情况。如果这不是提问者要问的,那么我可以再试一次:-)
  • 哦,我当然同意(这基本上是我自己回答的)。我只是误解了额外的传递错误。
  • 啊,所以对于稀疏删除 std::partition 是要走的路...:D
  • @KillianDS 你为什么删除你的答案?它提出了一个很好的观点,并且比这个更清楚地解释了缺少的性能增益,它走的是一条更加哲学的道路(当然,仍然是一个很好的答案)。
【解决方案2】:

std::remove 被指定用于前向迭代器。

从头到尾使用一对迭代器的方法要么会增加对迭代器的要求,从而降低函数的效用,要么会违反/恶化渐近复杂性保证。

【讨论】:

  • 这是一个好点。 stable_remove 采用前向迭代器和 unstable_remove 采用双向迭代器有多不优雅?不够优雅以至于委员会以这些理由将其驳回?我不认为提问者说现有的remove 不应该在标准中,只是它不应该被称为remove,因为这个名字没有宣传你(在他看来)付费为了稳定。
  • @NoSenseEtAl:好吧,memcpy 仅在迭代器类型为指向 POD 的指针时才有效(在 C++11 中,该要求变为可简单复制的指针)。或者,如果优化器可以确定迭代器的行为就像它一样,例如 vector::iterator 可能是指针的薄包装器。 deque&lt;shared_ptr&lt;string&gt;&gt; 有随机访问迭代器,但你不会很幸运地通过 memcpy 复制它,原因有两个(deque 不连续,你需要调用赋值运算符来增加引用计数)。
  • @SteveJessop 是的,我曾经在 deque 上犯过那个确切的错误:D 我不记得 ms 究竟使用 memcpy 是为了什么,但我记得它在某处被使用过......无论哪种方式,它都不是 RA 的副本范围。我错了
  • 在 A 处评论:它没有抓住重点。有人提到 std::distance() - 同名,显然 FW 它和 RA 它的实现不同。
【解决方案3】:

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

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

【讨论】:

  • 如果你要链接到一个提案,你应该链接到actual proposal,而不是它的旧草稿。至于默认是否应该是“不稳定”的形式,我认为基本上每个想要从某些东西中删除元素的人都会感到惊讶。拥有更高性能的remove 的能力是重要的;不要误会我的意思。但是当行为如此出乎意料时,要捍卫它应该是默认的想法要困难得多。
  • @NicolBolas 如果默认值效率低下或令人惊讶,则不应存在默认值。应该有两个函数名为stable_removeunstable_remove,所以选择是有意识的,而不是无意识的。这不适用于std::sort,因为不稳定的版本效率更高且不足为奇,因此它是默认值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-07
  • 1970-01-01
  • 2021-03-03
  • 1970-01-01
相关资源
最近更新 更多