Проблема C++0x: вставка постоянного времени в std::set

Согласно этой странице, я могу добиться вставки с постоянным временем, если использую

iterator std::set::insert ( iterator position, const value_type& x );

и итератор position, который я предоставляю, непосредственно «предшествует» правильной (по порядку) точке вставки.

Теперь меня интересует случай, когда я знаю, что значение, которое я вставляю, идет в конце (поскольку оно самое большое), например:

set<int> foo = {1, 2, 3};
foo.insert(4); // this is an inefficient insert

В соответствии с вышеуказанным критерием я должен передать последний элемент foo.end()-1 в insert не foo.end(). Правильно ли я понимаю? Что произойдет, если я пройду foo.end()? Будет ли это вставка O(log n) или O(1). Итак, варианты:

// Option A
foo.insert(foo.end()-1, 4);

// Option B
foo.insert(foo.end(), 4);

// Safer version of Option A
if(foo.empty())
    foo.insert(4);
else
    foo.insert(foo.end()-1, 4);

Я спрашиваю, потому что пишу функцию, созданную по шаблону контейнера. Я хочу вставить элемент (который, как я знаю, является самым большим) в конец любого переданного контейнера. Использование «Варианта A» выше имеет другое поведение для контейнера, такого как vector:

foo.insert(foo.end()-1, 4);
// result is {1, 2, 3, 4} if foo is an std::set
// result is {1, 2, 4, 3} if foo is an std::vector

Как предполагает @Bo_Persson, проблема здесь в том, что С++ 03 говорит «логарифмический в целом, но амортизированная константа, если t вставляется сразу после p». в то время как C ++ 0x говорит «логарифмический в целом, но амортизированная константа, если t вставляется прямо перед p».

PS: я использую GCC 4.5 на Ubuntu 11.04 с включенной поддержкой C++0x.

Изменить: я провел эмпирические тесты с включенной и отключенной поддержкой С++ 0x и ">опубликовал результаты в ответе. По сути, вывод состоит в том, что так же хорошо (и, очевидно, безопаснее) предоставить end() в качестве подсказки для вставки. Однако, очевидно, это всего лишь эмпирическое наблюдение. Стандарт, как уже говорилось, все еще сбивает с толку в этом аспекте.


person Alan Turing    schedule 03.07.2011    source источник


Ответы (4)


Является ли обманом запуск теста вместо чтения спецификаций библиотеки?

Для g++-4.4 -O2 для целых чисел 0 <= i < 5000000 мое время работы для стандартной вставки равно

real    0m14.952s
user    0m14.665s
sys 0m0.268s

и мое время работы для вставки с использованием end() в качестве подсказки

real    0m4.373s
user    0m4.148s
sys 0m0.224s

Насколько я могу судить, вставка в end() - 1 так же быстра, но более обременительна в использовании, потому что end() - 1 — недопустимая операция (вы должны использовать operator--()), и она дает сбой, если набор оказывается пустым.

#include <set>

typedef std::set<int> Set;

void insert_standard(Set& xs, int x)
{
    xs.insert(x);
}

void insert_hint_end(Set& xs, int x)
{
    xs.insert(xs.end(), x);
}

int main()
{
    const int cnt = 5000000;
    Set xs;
    for (int i = 0; i < cnt; i++) {
        // insert_hint_end(xs, i);
        insert_standard(xs, i);
    }
}
person antonakos    schedule 03.07.2011
comment
Отличный тест. Я предполагаю, что поддержка C++0x не включена. - person Alan Turing; 04.07.2011

Не совсем ясно, должен ли position указывать до или после точки вставки. Некоторые реализации работают и с тем, и с другим.

С другой стороны, если вам нужно разное поведение для разных контейнеров, почему бы вам просто не написать две перегрузки для вашей функции: одну для контейнеров с функцией push_back и одну для std::set.

person Bo Persson    schedule 03.07.2011
comment
Было бы неплохо проверить пару текущих реализаций, чтобы увидеть, как это обрабатывается. Я считаю, что в GCC было изменение, когда они перешли от проверки пары соседних значений к проверке только подсказки, а затем прерыванию. Но поскольку end() не может быть вычислено, рискну предположить, что его можно использовать для успешного намека на вставку элемента, который больше всех существующих; это было бы разумно и легко реализовать. - person Kerrek SB; 03.07.2011
comment
Проблема здесь в том, что C++03 говорит, что логарифмический в целом, но амортизированная константа, если t вставляется сразу после p. в то время как C++0x говорит, что логарифмическая в целом, но амортизированная константа, если t вставляется прямо перед p. Что делать плохому компилятору? :-) - person Bo Persson; 03.07.2011
comment
@Bo Persson: Я бы скорее сказал, что должен делать бедный писатель библиотеки? И кажется очевидным, что он должен следовать стандарту, для которого он кодирует, т.е. использовать стандарт C++0x для варианта библиотеки C++0x (та, что с emplace* методами и всем остальным). Я думаю, что это изменение было сделано для того, чтобы было легче приспособить корпус end(). - person Matthieu M.; 03.07.2011
comment
@Matthieu - Но это означает ломать старый код, что нехорошо. Я думаю, что это изменение может быть вызвано новым требованием С++ 0x для мультимножества/карты, чтобы эквивалентные узлы сохранялись в порядке вставки. - person Bo Persson; 03.07.2011
comment
@Bo Persson: обратите внимание, что вы нарушаете не семантику кода, а только аспект производительности. Ваша догадка кажется лучше моей, так как раньше не было никакой гарантии. - person Matthieu M.; 03.07.2011
comment
Интересно, можно ли рассматривать end() как особый случай. Таким образом, если вы пройдете end(), производительность будет такой же, как если бы вы прошли end()-1. Я думаю, что это разумно и остается в силе как по старому, так и по новому стандарту. - person Alan Turing; 03.07.2011
comment
Версия C++0x исправляет set<int> foo = {1, 2, 3}; foo.insert(some_iterator, 0), позволяя использовать foo.begin() как some_iterator? - person Ken Bloom; 04.07.2011
comment
GCC 4.6 ссылается на эту документацию. - person Kerrek SB; 04.07.2011
comment
@Kerrek, да, но я предполагаю, что это GCC без включенного C++0x (не то чтобы это было бы иначе, но я просто говорю). Я думаю, что новый стандарт С++ 0x имеет немного другую формулировку, как объясняет @Bo. Я надеюсь, что три случая в документе, на который вы ссылаетесь, останутся как есть. - person Alan Turing; 04.07.2011

Только предоставление итератора, который падает сразу после нового значения, имеет смысл.

Это связано с тем, что в наборе из N элементов имеется N+1 возможных точек вставки. Существует итератор, который идет после значения выше, чем у любого ранее существовавшего элемента, но нет итератора, который стоит перед значением перед всеми элементами.

person Ben Voigt    schedule 04.07.2011
comment
Я не думал об этом. Это отличный способ выразить это. Стандарт для std::set должен отражать этот факт. - person Alan Turing; 04.07.2011

Следуя по стопам @antonakos, я расширяю решение «мошенничества» и провожу эмпирический тест. Я использую GCC 4.5 с оптимизацией (-02) и рассматриваю как случай, когда поддержка C++0x не включена, так и когда она с -std=c++0x. Результаты для 40 000 000 вставок следующие (с указанием системного времени, так как другие значения в этом случае не являются особыми):

  • Without C++0x support
    • No hint: 26.6 seconds
    • Подсказка на end(): 5,71 секунды
    • Подсказка на --end(): 5,84 секунды
  • With C++0x support enabled
    • No hint: 29.2 seconds
    • Подсказка на end(): 5,34 секунды
    • Подсказка на --end(): 5,54 секунды

Вывод: GCC (с включенным C++0x или без него) вставляет эффективно, когда в качестве подсказки вставки указывается end().

Код, который я использовал, основан на коде @antonakos:

#include <set>
typedef std::set<int> Set;

void insert_standard(Set & xs, int x) {
    xs.insert(x);
}

void insert_hint_end(Set & xs, int x) {
    xs.insert(xs.end(), x);
}

void insert_hint_one_before_end(Set & xs, int x) {
    xs.insert(--xs.end(), x);
}

int main() {
    const int cnt = 40000000;
    Set xs;
    xs.insert(0);
    for (int i = 1; i < cnt; i++) {
        //insert_standard(xs, i);
        //insert_hint_one_before_end(xs, i);
        insert_hint_end(xs, i);
    }

    return 0;
}
person Alan Turing    schedule 03.07.2011