Когда используется метод std:: multimap
В настоящее время я экспериментирую с некоторым использованием stl-datastructures. Однако я до сих пор не уверен, когда использовать какой и когда использовать определенную комбинацию. В настоящее время я пытаюсь выяснить, когда использование std::multimap
имеет смысл. Насколько я вижу, можно легко создать собственную реализацию мультимапа, объединив std::map
и std::vector
. Поэтому я оставляю вопрос, когда нужно использовать каждую из этих структур данных.
- Простота: std:: multimap определенно проще использовать, потому что не нужно обрабатывать дополнительное вложение. Однако доступ к целому ряду элементов в качестве объемного может потребоваться скопировать данные из итераторов в другую структуру данных (например, a
std::vector
).
- Скорость. Локальность вектора, скорее всего, делает итерацию по диапазону равного элемента намного быстрее, поскольку оптимизируется использование кеша. Однако я предполагаю, что
std::multimaps
также имеет много оптимизационных трюков за спиной, чтобы сделать итерацию по равным элементам как можно быстрее. Также, возможно, для правильного выбора диапазона элементов можно оптимизировать для std::multimaps
.
Чтобы опробовать проблемы со скоростью, я сделал несколько простых сравнений, используя следующую программу:
#include <stdint.h>
#include <iostream>
#include <map>
#include <vector>
#include <utility>
typedef std::map<uint32_t, std::vector<uint64_t> > my_mumap_t;
const uint32_t num_partitions = 100000;
const size_t num_elements = 500000;
int main() {
srand( 1337 );
std::vector<std::pair<uint32_t,uint64_t>> values;
for( size_t i = 0; i <= num_elements; ++i ) {
uint32_t key = rand() % num_partitions;
uint64_t value = rand();
values.push_back( std::make_pair( key, value ) );
}
clock_t start;
clock_t stop;
{
start = clock();
std::multimap< uint32_t, uint64_t > mumap;
for( auto iter = values.begin(); iter != values.end(); ++iter ) {
mumap.insert( *iter );
}
stop = clock();
std::cout << "Filling std::multimap: " << stop - start << " ticks" << std::endl;
std::vector<uint64_t> sums;
start = clock();
for( uint32_t i = 0; i <= num_partitions; ++i ) {
uint64_t sum = 0;
auto range = mumap.equal_range( i );
for( auto iter = range.first; iter != range.second; ++iter ) {
sum += iter->second;
}
sums.push_back( sum );
}
stop = clock();
std::cout << "Reading std::multimap: " << stop - start << " ticks" << std::endl;
}
{
start = clock();
my_mumap_t mumap;
for( auto iter = values.begin(); iter != values.end(); ++iter ) {
mumap[ iter->first ].push_back( iter->second );
}
stop = clock();
std::cout << "Filling my_mumap_t: " << stop - start << " ticks" << std::endl;
std::vector<uint64_t> sums;
start = clock();
for( uint32_t i = 0; i <= num_partitions; ++i ) {
uint64_t sum = 0;
auto range = std::make_pair( mumap[i].begin(), mumap[i].end() );
for( auto iter = range.first; iter != range.second; ++iter ) {
sum += *iter;
}
sums.push_back( sum );
}
stop = clock();
std::cout << "Reading my_mumap_t: " << stop - start << " ticks" << std::endl;
}
}
Как я подозревал, это зависит в основном от соотношения между num_partitions
и num_elements
, поэтому я все еще здесь не понимаю. Вот несколько примеров:
Для num_partitions = 100000
и num_elements = 1000000
Filling std::multimap: 1440000 ticks
Reading std::multimap: 230000 ticks
Filling my_mumap_t: 1500000 ticks
Reading my_mumap_t: 170000 ticks
Для num_partitions = 100000
и num_elements = 500000
Filling std::multimap: 580000 ticks
Reading std::multimap: 150000 ticks
Filling my_mumap_t: 770000 ticks
Reading my_mumap_t: 140000 ticks
Для num_partitions = 100000
и num_elements = 200000
Filling std::multimap: 180000 ticks
Reading std::multimap: 90000 ticks
Filling my_mumap_t: 290000 ticks
Reading my_mumap_t: 130000 ticks
Для num_partitions = 1000
и num_elements = 1000000
Filling std::multimap: 970000 ticks
Reading std::multimap: 150000 ticks
Filling my_mumap_t: 710000 ticks
Reading my_mumap_t: 10000 ticks
Я не уверен, как интерпретировать эти результаты. Как бы вы определили правильную структуру данных? Существуют ли какие-либо дополнительные ограничения для деления, которые я, возможно, пропустил?
Ответы
Ответ 1
Трудно сказать, правильно ли работает ваш тест, поэтому я не могу комментировать цифры. Однако несколько общих моментов:
-
Почему multimap
, а не карта векторов: карты, мультиплексоры, наборы и мультимножества - это по существу одна и та же структура данных, и, как только у вас есть, тривиально просто указать все четыре. Итак, первый ответ: "почему бы и нет"?
-
Как это полезно: Multimaps - одна из тех вещей, которые вам нужны редко, но когда они вам нужны, вам действительно нужны.
-
Почему бы не опрокинуть мое собственное решение? Как я уже сказал, я не уверен в этих тестах, но даже если вы можете сделать что-то еще, что не хуже стандартного контейнера (что я сомневаюсь), тогда вы должны учитывать общее бремя получения права, тестируя его и поддерживать его. Представьте себе мир, в котором вы будете облагаться налогом за каждую строку кода, которую вы написали (это предложение Степанова). Повторно используйте компоненты промышленного стандарта, когда это возможно.
Наконец, здесь типичный способ повторения мультимапа:
for (auto it1 = m.cbegin(), it2 = it1, end = m.cend(); it1 != end; it1 = it2)
{
// unique key values at this level
for ( ; it2 != end && it2->first == it1->first; ++it2)
{
// equal key value (`== it1->first`) at this level
}
}
Ответ 2
Вы забыли одну очень важную альтернативу: не все последовательности созданы равными.
В частности, почему a vector
, а не deque
или list
?
Используя list
A std::map<int, std::list<int> >
должен быть примерно эквивалентен std::multimap<int, int>
, поскольку list
также основан на node.
Используя deque
A deque
является контейнером по умолчанию, который вы используете, когда не знаете, для чего идти, и у вас нет особых требований.
Что касается vector
, вы можете увеличить скорость чтения (не намного) для более быстрых операций push
и pop
.
Используя deque
и некоторые очевидные оптимизации, я получаю:
const uint32_t num_partitions = 100000;
const size_t num_elements = 500000;
Filling std::multimap: 360000 ticks
Filling MyMumap: 530000 ticks
Reading std::multimap: 70000 ticks (0)
Reading MyMumap: 30000 ticks (0)
Или в "плохом" случае:
const uint32_t num_partitions = 100000;
const size_t num_elements = 200000;
Filling std::multimap: 100000 ticks
Filling MyMumap: 240000 ticks
Reading std::multimap: 30000 ticks (0)
Reading MyMumap: 10000 ticks (0)
Таким образом, чтение выполняется безоговорочно быстрее, но заполнение также медленнее.
Ответ 3
Карта векторов поставляется с служебными данными памяти для емкости каждого вектора. std::vector
обычно выделяет пространство для большего количества элементов, чем у вас на самом деле. Это не может быть большой проблемой для вашего приложения, но это еще один компромисс, который вы не рассматривали.
Если вы делаете много чтений, то время поиска O (1) unordered_multimap
может быть лучшим выбором.
Если у вас достаточно современный компилятор (и учитывая наличие ключевого слова auto
, то вы это делаете), то в целом вам будет сложно избивать стандартные контейнеры с точки зрения производительности и надежности. Люди, которые их написали, являются экспертами. Я бы всегда начинал со стандартного контейнера, который наиболее легко выражает то, что вы хотите сделать. Профилируйте свой код рано и часто, и если он не работает достаточно быстро, найдите способы его улучшения (например, при использовании контейнеров unordered_
при чтении в большинстве случаев).
Итак, чтобы ответить на ваш оригинальный вопрос, если вам нужен ассоциативный массив значений, где эти значения не будут уникальными, то использование std::multimap
определенно имеет смысл.