Подтвердить что ты не робот

Когда используется метод 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

Я не уверен, как интерпретировать эти результаты. Как бы вы определили правильную структуру данных? Существуют ли какие-либо дополнительные ограничения для деления, которые я, возможно, пропустил?

4b9b3361

Ответ 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 определенно имеет смысл.