为啥使用 std::multiset 作为优先级队列比使用 std::priority_queue 更快?
Posted
技术标签:
【中文标题】为啥使用 std::multiset 作为优先级队列比使用 std::priority_queue 更快?【英文标题】:Why is using a std::multiset as a priority queue faster than using a std::priority_queue?为什么使用 std::multiset 作为优先级队列比使用 std::priority_queue 更快? 【发布时间】:2011-08-19 05:49:07 【问题描述】:我尝试用 std::priority_queue 替换 std::multiset。但我对速度结果感到失望。算法运行时间增加50%...
下面是对应的命令:
top() = begin();
pop() = erase(knn.begin());
push() = insert();
我对priority_queue 实现的速度感到惊讶,我期待不同的结果(对PQ 更好)...从概念上讲,多重集被用作优先级队列。为什么即使使用-O2
,优先级队列和多重集的性能也会如此不同?
十个结果的平均值,MSVS 2010,Win XP,32 位,方法 findAllKNN2()(请参见下文)
MS
N time [s]
100 000 0.5
1 000 000 8
PQ
N time [s]
100 000 0.8
1 000 000 12
什么可能导致这些结果?未对源代码进行其他更改...感谢您的帮助...
MS 实施:
template <typename Point>
struct TKDNodePriority
KDNode <Point> *node;
typename Point::Type priority;
TKDNodePriority() : node ( NULL ), priority ( 0 )
TKDNodePriority ( KDNode <Point> *node_, typename Point::Type priority_ ) : node ( node_ ), priority ( priority_ )
bool operator < ( const TKDNodePriority <Point> &n1 ) const
return priority > n1.priority;
;
template <typename Point>
struct TNNeighboursList
typedef std::multiset < TKDNodePriority <Point> > Type;
;
方法:
template <typename Point>
template <typename Point2>
void KDTree2D <Point>::findAllKNN2 ( const Point2 * point, typename TNNeighboursList <Point>::Type & knn, unsigned int k, KDNode <Point> *node, const unsigned int depth ) const
if ( node == NULL )
return;
if ( point->getCoordinate ( depth % 2 ) <= node->getData()->getCoordinate ( depth % 2 ) )
findAllKNN2 ( point, knn, k, node->getLeft(), depth + 1 );
else
findAllKNN2 ( point, knn, k, node->getRight(), depth + 1 );
typename Point::Type dist_q_node = ( node->getData()->getX() - point->getX() ) * ( node->getData()->getX() - point->getX() ) +
( node->getData()->getY() - point->getY() ) * ( node->getData()->getY() - point->getY() );
if (knn.size() == k)
if (dist_q_node < knn.begin()->priority )
knn.erase(knn.begin());
knn.insert ( TKDNodePriority <Point> ( node, dist_q_node ) );
else
knn.insert ( TKDNodePriority <Point> ( node, dist_q_node ) );
typename Point::Type dist_q_node_straight = ( point->getCoordinate ( node->getDepth() % 2 ) - node->getData()->getCoordinate ( node->getDepth() % 2 ) ) *
( point->getCoordinate ( node->getDepth() % 2 ) - node->getData()->getCoordinate ( node->getDepth() % 2 ) ) ;
typename Point::Type top_priority = knn.begin()->priority;
if ( knn.size() < k || dist_q_node_straight < top_priority )
if ( point->getCoordinate ( node->getDepth() % 2 ) < node->getData()->getCoordinate ( node->getDepth() % 2 ) )
findAllKNN2 ( point, knn, k, node->getRight(), depth + 1 );
else
findAllKNN2 ( point, knn, k, node->getLeft(), depth + 1 );
PQ 实施(较慢,为什么?)
template <typename Point>
struct TKDNodePriority
KDNode <Point> *node;
typename Point::Type priority;
TKDNodePriority() : node ( NULL ), priority ( 0 )
TKDNodePriority ( KDNode <Point> *node_, typename Point::Type priority_ ) : node ( node_ ), priority ( priority_ )
bool operator < ( const TKDNodePriority <Point> &n1 ) const
return priority > n1.priority;
;
template <typename Point>
struct TNNeighboursList
typedef std::priority_queue< TKDNodePriority <Point> > Type;
;
方法:
template <typename Point>
template <typename Point2>
void KDTree2D <Point>::findAllKNN2 ( const Point2 * point, typename TNNeighboursList <Point>::Type & knn, unsigned int k, KDNode <Point> *node, const unsigned int depth ) const
if ( node == NULL )
return;
if ( point->getCoordinate ( depth % 2 ) <= node->getData()->getCoordinate ( depth % 2 ) )
findAllKNN2 ( point, knn, k, node->getLeft(), depth + 1 );
else
findAllKNN2 ( point, knn, k, node->getRight(), depth + 1 );
typename Point::Type dist_q_node = ( node->getData()->getX() - point->getX() ) * ( node->getData()->getX() - point->getX() ) +
( node->getData()->getY() - point->getY() ) * ( node->getData()->getY() - point->getY() );
if (knn.size() == k)
if (dist_q_node < knn.top().priority )
knn.pop();
knn.push ( TKDNodePriority <Point> ( node, dist_q_node ) );
else
knn.push ( TKDNodePriority <Point> ( node, dist_q_node ) );
typename Point::Type dist_q_node_straight = ( point->getCoordinate ( node->getDepth() % 2 ) - node->getData()->getCoordinate ( node->getDepth() % 2 ) ) *
( point->getCoordinate ( node->getDepth() % 2 ) - node->getData()->getCoordinate ( node->getDepth() % 2 ) ) ;
typename Point::Type top_priority = knn.top().priority;
if ( knn.size() < k || dist_q_node_straight < top_priority )
if ( point->getCoordinate ( node->getDepth() % 2 ) < node->getData()->getCoordinate ( node->getDepth() % 2 ) )
findAllKNN2 ( point, knn, k, node->getRight(), depth + 1 );
else
findAllKNN2 ( point, knn, k, node->getLeft(), depth + 1 );
【问题讨论】:
这是一次性观察还是一致观察? @unapersson:嗯,multiset
,作为一个搜索树,可以作为一个优先队列。我只是想知道哪些成员打了多少电话。
据我所见,std c++ 中的 maps/xtree 被实现为红黑二叉树。我不知道各个 C++ 供应商对优先级队列的具体实现,但最可取的是使用堆。在大 O 表示法中,两者都具有 n*log(n) 的复杂性,但我在某处读到堆具有很大的常数因子。有cmets吗?
@Mayank:标准没有指定有序容器(multiset
和朋友)是如何实现的,尽管复杂性要求几乎迫使它们成为平衡树。它确实指定priority_queue
必须使用标准堆算法(并且由于底层容器是受保护的成员,因此执行其他操作会破坏队列的可见行为)。
你是怎么测量的?请使用谷歌基准!有提供此工具的在线网站:quick-bench.com
【参考方案1】:
首先,作者没有提供导致上述性能下降的最小代码示例。 其次,这个问题是 8 年前提出的,我相信编译器对性能有很大的提升。
我做了一个基准示例,我在队列中取第一个元素,然后以另一个优先级推回(模拟推新元素而不创建一个),通过循环中数组 kNodesCount
中的元素计数来实现kRunsCount
迭代。我将priority_queue
与multiset
和multimap
进行比较。我决定包含multimap
以进行更精确的比较。它的简单测试非常接近作者的用例,我也尝试重现他在代码示例中使用的结构。
#include <set>
#include <type_traits>
#include <vector>
#include <chrono>
#include <queue>
#include <map>
#include <iostream>
template<typename T>
struct Point
static_assert(std::is_integral<T>::value || std::is_floating_point<T>::value, "Incompatible type");
using Type = T;
T x;
T y;
;
template<typename T>
struct Node
using Type = T;
Node<T> * left;
Node<T> * right;
T data;
;
template <typename T>
struct NodePriority
using Type = T;
using DataType = typename T::Type;
Node<T> * node = nullptr;
DataType priority = static_cast<DataType>(0);
bool operator < (const NodePriority<T> & n1) const noexcept
return priority > n1.priority;
bool operator > (const NodePriority<T> & n1) const noexcept
return priority < n1.priority;
;
// descending order by default
template <typename T>
using PriorityQueueList = std::priority_queue<T>;
// greater used because of ascending order by default
template <typename T>
using MultisetList = std::multiset<T, std::greater<T>>;
// greater used because of ascending order by default
template <typename T>
using MultimapList = std::multimap<typename T::DataType, T, std::greater<typename T::DataType>>;
struct Inner
template<template <typename> class C, typename T>
static void Operate(C<T> & list, std::size_t priority);
template<typename T>
static void Operate(PriorityQueueList<T> & list, std::size_t priority)
if (list.size() % 2 == 0)
auto el = std::move(list.top());
el.priority = priority;
list.push(std::move(el));
else
list.pop();
template<typename T>
static void Operate(MultisetList<T> & list, std::size_t priority)
if (list.size() % 2 == 0)
auto el = std::move(*list.begin());
el.priority = priority;
list.insert(std::move(el));
else
list.erase(list.begin());
template<typename T>
static void Operate(MultimapList<T> & list, std::size_t priority)
if (list.size() % 2 == 0)
auto el = std::move(*list.begin());
auto & elFirst = const_cast<int&>(el.first);
elFirst = priority;
el.second.priority = priority;
list.insert(std::move(el));
else
list.erase(list.begin());
;
template<typename T>
void doOperationOnPriorityList(T & list)
for (std::size_t pos = 0, len = list.size(); pos < len; ++pos)
// move top element and update priority
auto priority = std::rand() % 10;
Inner::Operate(list, priority);
template<typename T>
void measureOperationTime(T & list, std::size_t runsCount)
std::chrono::system_clock::time_point t1, t2;
std::uint64_t totalTime(0);
for (std::size_t i = 0; i < runsCount; ++i)
t1 = std::chrono::system_clock::now();
doOperationOnPriorityList(list);
t2 = std::chrono::system_clock::now();
auto castedTime = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
std::cout << "Run " << i << " time: " << castedTime << "\n";
totalTime += castedTime;
std::cout << "Average time is: " << totalTime / runsCount << " ms" << std::endl;
int main()
// consts
const int kNodesCount = 10'000'000;
const int kRunsCount = 10;
// prepare data
PriorityQueueList<NodePriority<Point<int>>> neighboursList1;
MultisetList<NodePriority<Point<int>>> neighboursList2;
MultimapList<NodePriority<Point<int>>> neighboursList3;
std::vector<Node<Point<int>>> nodes;
nodes.reserve(kNodesCount);
for (auto i = 0; i < kNodesCount; ++i)
nodes.emplace_back(decltype(nodes)::value_type nullptr, nullptr, 0,0 );
auto priority = std::rand() % 10;
neighboursList1.emplace(decltype(neighboursList1)::value_type &nodes.back(), priority );
neighboursList2.emplace(decltype(neighboursList2)::value_type &nodes.back(), priority );
neighboursList3.emplace(decltype(neighboursList3)::value_type priority, &nodes.back(), priority );
// do operation on data
std::cout << "\nPriority queue\n";
measureOperationTime(neighboursList1, kRunsCount);
std::cout << "\nMultiset\n";
measureOperationTime(neighboursList2, kRunsCount);
std::cout << "\nMultimap\n";
measureOperationTime(neighboursList3, kRunsCount);
return 0;
我已经使用 VS v15.8.9 使用 /Ox 进行了发布构建。查看 10 次运行中 10'000'000 个项目的结果:
Priority queue
Run 0 time: 764
Run 1 time: 933
Run 2 time: 920
Run 3 time: 813
Run 4 time: 991
Run 5 time: 862
Run 6 time: 902
Run 7 time: 1277
Run 8 time: 774
Run 9 time: 771
Average time is: 900 ms
Multiset
Run 0 time: 2235
Run 1 time: 1811
Run 2 time: 1755
Run 3 time: 1535
Run 4 time: 1475
Run 5 time: 1388
Run 6 time: 1482
Run 7 time: 1431
Run 8 time: 1347
Run 9 time: 1347
Average time is: 1580 ms
Multimap
Run 0 time: 2197
Run 1 time: 1885
Run 2 time: 1725
Run 3 time: 1671
Run 4 time: 1500
Run 5 time: 1403
Run 6 time: 1411
Run 7 time: 1420
Run 8 time: 1409
Run 9 time: 1362
Average time is: 1598 ms
嗯,如您所见,multiset
的性能与multimap
相同,而priority_queue
是最快的(大约快 43%)。那为什么会这样呢?
让我们从priority_queue
开始,C++ 标准并没有告诉我们如何实现一个或另一个容器或结构,但在大多数情况下它是基于binary heap(寻找 msvc 和 gcc 实现)!在priority_queue
的情况下,您无法访问除 top 之外的任何元素,您无法遍历它们、按索引获取,甚至获取最后一个元素(它为优化腾出了一些空间)。二叉堆的平均插入是 O(1),只有最坏的情况是 O(log n),删除是 O(log n),因为我们从底部取出元素然后搜索下一个高优先级。
multimap
和 multiset
怎么样。它们通常都在red-black binary tree 上实现(查找 msvc 和 gcc 实现),其中平均插入为 O(log n),删除平均为 O(log n)。
从这个角度来看,priority_queue
NEVER 可能比multiset
或multimap
慢。所以,回到你的问题,multiset
作为优先队列不比priority_queue
本身快。可能有很多原因,包括旧编译器上的原始priority_queue
实现或错误使用此结构(问题不包含最小的可行示例),除了作者没有提到编译标志或编译器版本,有时优化很重要变化。
更新 1 应@noɥʇʎԀʎzɐɹƆ 请求
不幸的是,我现在无法访问 linux 环境,但我安装了 mingw-w64,版本信息:g++.exe (x86_64-posix-seh,由草莓perl.com 项目构建) 8.3.0。使用的处理器与 Visual Studio 相同:处理器 Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz, 2001 Mhz, 4 Core(s), 8 Logical Processor(s)。
所以g++ -O2
的结果是:
Priority queue
Run 0 time: 775
Run 1 time: 995
Run 2 time: 901
Run 3 time: 807
Run 4 time: 930
Run 5 time: 765
Run 6 time: 799
Run 7 time: 1151
Run 8 time: 760
Run 9 time: 780
Average time is: 866 ms
Multiset
Run 0 time: 2280
Run 1 time: 1942
Run 2 time: 1607
Run 3 time: 1344
Run 4 time: 1319
Run 5 time: 1210
Run 6 time: 1129
Run 7 time: 1156
Run 8 time: 1244
Run 9 time: 992
Average time is: 1422 ms
Multimap
Run 0 time: 2530
Run 1 time: 1958
Run 2 time: 1670
Run 3 time: 1390
Run 4 time: 1391
Run 5 time: 1235
Run 6 time: 1088
Run 7 time: 1198
Run 8 time: 1071
Run 9 time: 963
Average time is: 1449 ms
您可能会注意到它与 msvc 的图片几乎相同。
更新 2 感谢@JorgeBellon
quick-bench.com 在线基准测试链接,请自行查看!
希望看到我的帖子的任何补充,干杯!
【讨论】:
我希望看到您在 -O2 上使用 GCC 运行相同的测试。 @noɥʇʎԀʎzɐɹƆ 我刚刚更新了答案,结果为 mingw-w64,因为我现在没有 linux 环境,你可以自己构建它,我已经更新了正确构建的代码在 g++ 下也用 ms 输出更新了 msvc 结果 好答案,关于优化标志和编译器版本,你们两个可能对以下内容感兴趣:quick-bench.com/cNc8AtverZGKuqDcuxS6IS14mVE【参考方案2】:您的编译器的优化设置似乎对性能有很大影响。在下面的代码中,multiset 在没有优化的情况下轻松击败了基于向量和基于双端队列的 priority_queue。但是,通过“-O3”优化,基于向量的优先级队列胜过一切。现在,这些实验是在带有 GCC 的 Linux 上运行的,所以也许你会在 Windows 上得到不同的结果。我相信启用优化可能会消除 STL 向量中的许多错误检查行为。
没有优化:
pq-w-向量:79.2997ms
pq-w-deque: 362.366ms
pq-w-multiset:34.649ms
使用 -O2 优化:
pq-w-向量:8.88154ms
pq-w-deque: 17.5233ms
pq-w-multiset:12.5539ms
使用 -O3 优化:
pq-w-向量:7.92462ms
pq-w-deque: 16.8028ms
pq-w-multiset: 12.3208ms
Test Harness(别忘了用-lrt链接):
#include <iostream>
#include <queue>
#include <deque>
#include <set>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
using namespace std;
template <typename T>
double run_test(T& pq, int size, int iterations)
struct timespec start, end;
for(int i = 0; i < size; ++i)
pq.push(rand());
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &start);
for(int i = 0; i < iterations; ++i)
if(rand()%2)
pq.pop();
else
pq.push(rand());
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &end);
end.tv_sec -= start.tv_sec;
end.tv_nsec -= start.tv_nsec;
if (end.tv_nsec < 0)
--end.tv_sec;
end.tv_nsec += 1000000000ULL;
return (end.tv_sec*1e3 + end.tv_nsec/1e6);
template <class T>
class multiset_pq: public multiset<T>
public:
multiset_pq(): multiset<T>() ;
void push(T elm) this->insert(elm);
void pop() if(!this->empty()) this->erase(this->begin());
const T& top() return *this->begin();
;
int main(void)
const int size = 5000;
const int iterations = 100000;
priority_queue<int, vector<int> > pqv;
priority_queue<int, deque<int> > pqd;
multiset_pq<int> pqms;
srand(time(0));
cout<<"pq-w-vector: "<<run_test(pqv, size, iterations)<<"ms"<<endl;
cout<<"pq-w-deque: "<<run_test(pqd, size, iterations)<<"ms"<<endl;
cout<<"pq-w-multiset: "<<run_test(pqms, size, iterations)<<"ms"<<endl;
return 0;
【讨论】:
查看 Matt Godbolt 的编译器资源管理器:godbolt.org。您可以查看编译器的反汇编并比较改变编译器优化时产生的结果。 插入 rand() 实际上并不代表现实生活中的情况,尤其是基于作者代码示例。priority_queue
默认也是基于 vector
【参考方案3】:
据我了解,priority_queue
的实现是罪魁祸首。 priority_queue
被实现(在下面)作为专门的 vector
或 deque
。因为priority_queue 需要有随机访问迭代器。当您将项目弹出或推送到priority_queue
时,队列中的剩余项目需要复制到空白空间,插入也是如此。 multi_set
基于键。
编辑:非常抱歉,multi_set 不是基于哈希键。出于某种原因,我将它与 multi_map 混淆了。但是 multi_set 是一个多重排序的关联容器,根据 key 来存储元素,key 和 value 一样。由于元素存储在multi_set
中的方式,它
...具有重要的属性 将新元素插入到
multi_set
不会失效 指向现有的迭代器 元素。从 a 中删除一个元素multi_set
也不会失效 任何迭代器,当然,除了 实际上指向的迭代器 正在被删除的元素。
-- 引用自SGI documentation。
这意味着multi_set
的存储不是线性的,因此性能提升。
【讨论】:
multisets
不是基于哈希表 unordered_multisets
是
Insert/delete 在任何一种情况下都是 O(lg n) 并且 multiset
不是哈希表。 -1.
@BradTilley:您确实意识到这是在 C++11 获得批准之前编写的,对吧?如果那是你的主要动机,你能取消你的反对票吗?我已经为我的错误吸取了教训:) 这个答案是在 2011 年 5 月,而 C++11 是在 11 年 8 月获得批准的。谢谢。【参考方案4】:
这个非综合基准取自priority_queue 的实际使用。将this file 作为标准输入输入以运行基准测试。
// TOPOSORT 2
// This short function computes the lexicographically smallest toposort.
// priority_queue vs multiset benchmark
#include <vector>
#include <queue>
#include <set>
#include <unordered_set>
#include <ctime>
#include <chrono>
#include <iostream>
// https://***.com/a/13772771/1459669
#ifdef _WIN32
#include <intrin.h>
#else
#include <x86intrin.h>
#endif
using namespace std;
constexpr int MAXN = 100001;
struct Tail
int observation, number;
;
typedef vector<vector<Tail>> AdjList;
int N, M;
void computeNumIncomingEdges(int observationID, AdjList adjacency_list,
int *numIncomingEdges)
for (int node = 0; node <= N; ++node)
numIncomingEdges[node] = 0;
for (int node = 1; node <= N; ++node)
for (Tail tail : adjacency_list[node])
if (tail.observation <= observationID)
numIncomingEdges[tail.number]++;
template<class T>
vector<int> toposort2_PQ(int observationID, AdjList adjacency_list)
vector<int> sortedElements;
priority_queue<int, T, std::greater<int>> S;
static int numIncomingEdges[MAXN];
computeNumIncomingEdges(observationID, adjacency_list, numIncomingEdges);
for (int node = 1; node <= N; ++node)
if (numIncomingEdges[node] == 0)
S.push(node);
while (!S.empty())
auto n = S.top();
S.pop();
sortedElements.push_back(n);
for (int _ = adjacency_list[n].size() - 1; _ >= 0; --_)
Tail m = adjacency_list[n][_];
if (m.observation <= observationID)
adjacency_list[n].pop_back();
numIncomingEdges[m.number]--;
if (numIncomingEdges[m.number] == 0)
S.push(m.number);
bool graphStillHasEdges = false;
for (int node = 1; node <= N; ++node)
if (numIncomingEdges[node] > 0)
graphStillHasEdges = true;
break;
return sortedElements;
vector<int> toposort2_multiset(int observationID, AdjList adjacency_list)
vector<int> sortedElements;
multiset<int, std::greater<int>> S;
static int numIncomingEdges[MAXN];
computeNumIncomingEdges(observationID, adjacency_list, numIncomingEdges);
for (int node = 1; node <= N; ++node)
if (numIncomingEdges[node] == 0)
S.insert(node);
while (!S.empty())
int n = *S.begin();
S.erase(S.begin());
sortedElements.push_back(n);
for (int _ = adjacency_list[n].size() - 1; _ >= 0; --_)
Tail m = adjacency_list[n][_];
if (m.observation <= observationID)
adjacency_list[n].pop_back();
numIncomingEdges[m.number]--;
if (numIncomingEdges[m.number] == 0)
S.insert(m.number);
bool graphStillHasEdges = false;
for (int node = 1; node <= N; ++node)
if (numIncomingEdges[node] > 0)
graphStillHasEdges = true;
break;
return sortedElements;
int main()
scanf("%d %d", &N, &M);
AdjList adjacency_list(MAXN);
for (int observation = 0; observation < M; ++observation)
int observationSize;
scanf("%d", &observationSize);
int head;
scanf("%d", &head);
for (int i = 0; i < observationSize - 1; ++i)
int tail;
scanf("%d", &tail);
Tail to_insert;
to_insert.observation = observation;
to_insert.number = tail;
adjacency_list[head].push_back(to_insert);
head = tail;
for (int i = 0; i < 5; ++i)
auto start_pq = std::chrono::high_resolution_clock::now();
toposort2_PQ<vector<int>>(3182, adjacency_list);
auto end_pq = std::chrono::high_resolution_clock::now();
auto start_pq_dq = std::chrono::high_resolution_clock::now();
toposort2_PQ<deque<int>>(3182, adjacency_list);
auto end_pq_dq = std::chrono::high_resolution_clock::now();
auto start_ms = std::chrono::high_resolution_clock::now();
toposort2_multiset(3182, adjacency_list);
auto end_ms = std::chrono::high_resolution_clock::now();
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end_pq-start_pq).count() << ' ' << std::chrono::duration_cast<std::chrono::microseconds>(end_pq_dq-start_pq_dq).count() << ' ' << std::chrono::duration_cast<std::chrono::microseconds>(end_ms-start_ms).count() << endl;
将 clang++ 与 -O2 结合使用让我受益匪浅:
31622 37891 54884
27092 33919 54878
27324 35870 51427
27961 35348 53170
26746 34753 54191
总之,带有向量的priority_queue 始终获胜。第二位是带有双端队列的priority_queue,最后是一个multiset。
【讨论】:
当使用的类型是更大的类型而不是int
时会发生什么?原始问题使用具有两个字段、一个指针和某种数字(int/float/double)的结构。由于要复制的数据较大,这可能会增加 PQ 的执行时间。
您的帖子实际上并没有回答主题的问题,为什么一种或另一种数据结构更快?你只是说:“看,在我的环境中,在这个特殊算法中使用这个特定数据会更快”。以上是关于为啥使用 std::multiset 作为优先级队列比使用 std::priority_queue 更快?的主要内容,如果未能解决你的问题,请参考以下文章
std::multimap::find 将返回哪个元素,类似地 std::multiset::find?
如何在不重载 `operator()`、`std::less`、`std::greater` 的情况下为`std::multiset` 提供自定义比较器?
C++ std::multiset返回值 has no member named ‘first’