Почему unordered_map "find + insert" быстрее, чем "insert + check for success"?
Я использую unordered_map как разреженный 3D-массив (128 x 128 x 128) для вставки значений в сетку, если ячейка сетки по-прежнему свободна.
До сих пор я всегда проверял с помощью find(), если ячейка свободна, и если она есть, то я добавил элемент, используя insert() или emplace().
Теперь я обнаружил, что могу использовать возвращаемое значение insert и emplace, чтобы проверить, был ли добавлен элемент или уже был элемент с тем же ключом внутри карты. Я думал, что это может повысить производительность, поскольку я могу полностью удалить использование find.
Как оказалось, вместо того, чтобы улучшать производительность, вставляя без поиска, производительность действительно уменьшалась, и я не уверен, почему.
Я уменьшил свое приложение до этого примера, где точки генерируются случайным образом, а затем вставляются в сетку.
#include <unordered_map>
#include <random>
#include <chrono>
#include <iostream>
#include <math.h>
#include <algorithm>
#include <string>
using std::cout;
using std::endl;
using std::chrono::high_resolution_clock;
using std::chrono::milliseconds;
using std::chrono::duration_cast;
using std::unordered_map;
int num_elements = 5'000'000;
void findThenInsert(){
cout << endl << "find and emplace" << endl;
auto start = high_resolution_clock::now();
std::mt19937 gen(123);
std::uniform_real_distribution<> dis(0, 128);
unordered_map<int, int> grid;
int count = 0;
for(int i = 0; i < num_elements; i++){
float x = dis(gen);
float y = dis(gen);
float z = (cos(x*0.1) * sin(x*0.1) + 1.0) * 64.0;
int index = int(x) + int(y) * 128 + int(z) * 128 * 128;
auto it = grid.find(index);
if(it == grid.end()){
grid.emplace(index, count);
count++;
}
}
cout << "elements: " << count << endl;
cout << "load factor: " << grid.load_factor() << endl;
auto end = high_resolution_clock::now();
long long duration = duration_cast<milliseconds>(end - start).count();
float seconds = duration / 1000.0f;
cout << seconds << "s" << endl;
}
void insertThenCheckForSuccess(){
cout << endl << "emplace and check success" << endl;
auto start = high_resolution_clock::now();
std::mt19937 gen(123);
std::uniform_real_distribution<> dis(0, 128);
unordered_map<int, int> grid;
int count = 0;
for(int i = 0; i < num_elements; i++){
float x = dis(gen);
float y = dis(gen);
float z = (cos(x*0.1) * sin(x*0.1) + 1.0) * 64.0;
int index = int(x) + int(y) * 128 + int(z) * 128 * 128;
auto it = grid.emplace(index, count);
if(it.second){
count++;
}
}
cout << "elements: " << count << endl;
cout << "load factor: " << grid.load_factor() << endl;
auto end = high_resolution_clock::now();
long long duration = duration_cast<milliseconds>(end - start).count();
float seconds = duration / 1000.0f;
cout << seconds << "s" << endl;
}
int main(){
findThenInsert();
insertThenCheckForSuccess();
}
В обоих случаях размер карты составляет 82901, поэтому я предполагаю, что результат будет точно таким же.
find and emplace: 0.937s
emplace then check: 1.268s
Ответы
Ответ 1
Проблема в том, что спецификация emplace
для ассоциативных контейнеров в действительности требует распределения даже в случае сбоя; стоимость этого распределения и перераспределения доминирует над стоимостью неудавшегося зонда в стратегии find-then-insert.
Это связано с тем, что emplace
указан для emplace-construct value_type
(т.е. pair<Key const, T>
) из его пересылаемых аргументов; только после того, как он построил пару, он может хешировать ключ, чтобы проверить, присутствует ли он уже. (Он не может просто принять первый аргумент, потому что это может быть std::piecewise_construct
.) Он также не может построить pair
в автоматическом хранилище, а затем переместить его в node, потому что emplace
не указан для того чтобы требовать скопированного или даже подвижного value_type
, поэтому он должен выполнить потенциально дорогое распределение node при каждом вызове. (Обратите внимание, что упорядоченные ассоциативные контейнеры имеют одинаковую проблему, но стоимость O (log n) для зонда более значительна по сравнению со стоимостью распределения.)
Если в большинстве случаев ваши вставки не будут успешными, вам лучше использовать find-then-emplace поверх emplace-then-test. Вы также можете использовать insert
, если вы убедитесь, что вы вызываете перегрузку value_type
, а не шаблон, который пересылается в emplace
.
Это (возможно) исправлено в С++ 17, которое (должно) иметь try_emplace
, с аналогичной семантикой, чтобы заменить, но улучшило производительность в случае сбоя, (Разница в семантике заключается в том, что отображаемый тип не создается emplace в случае сбоя, что позволяет, например, хранить unique_ptr
в качестве отображаемого типа.)
Ответ 2
Я думаю, проблема в том, что вы используете emplace
вместо insert
. Проблема в том, что функции emplace в ассоциативных контейнерах обычно выделяют память для node, даже если ключ уже присутствует. Таким образом, если вы регулярно размещаете дубликаты, эти распределения памяти теряются. Если вы использовали вставку, это будет делать только распределение памяти, если вставка выполнена успешно.
Скотт Мейерс говорит, чтобы предпочесть только функции emplace над функциями вставки, если "контейнер не будет отклонять добавленное значение из-за того, что он является дубликатом"
Я не могу точно воспроизвести ваши результаты, но мое тестирование показывает, что вставка (не emplace), а затем тест еще быстрее, чем поиск, затем emplace:
auto it = grid.insert({index, count});
Это решение также может зависеть от того, насколько дорого стоило создавать свой тип значения. find
не нужно создавать тип значения, ему просто нужен ключ. Но emplace
и insert
нужен ключ и тип значения, поэтому в тех случаях, когда это дорого, чтобы создать значение, он может быстрее использовать find и только создать значение, если вам нужно. В этом случае ваше значение равно только int
, поэтому я ожидаю, что insert
или emplace
всегда будет побеждать find-then-emplace.