Могут ли алгоритмы STL и back_inserter предварительно выделять пространство?

Если у меня есть что-то вроде:

vector<int> longVector = { ... };
vector<int> newVector;
transform(longVector.begin(), longVector.end(), back_inserter(newVector),
          [] (int i) { return i * i; });

Сможет ли STL предварительно выделить место в newVector перед обработкой и добавлением новых элементов? Я знаю, что это не требование алгоритма, но сможет ли «хорошая» реализация оптимизировать это? Или, в таком случае, я должен предпочесть добавить newVector.reserve(longVector.size()); раньше? Я не обязательно спрашиваю, работает ли каждая реализация stdlib или нет (хотя, если кто-то знает конкретные примеры, это было бы здорово), но больше, возможно ли это вообще (и ожидается), учитывая интерфейс и требования алгоритмов.

Вопрос относится к нескольким алгоритмам STL, transform, copy, move, fill_n, ... И не только к back_inserter, но также front_inserter и inserter, я полагаю.

РЕДАКТИРОВАТЬ: Для ясности я имею в виду, может ли stdlib предоставлять конкретные реализации, например, transform, для случая, когда выходной итератор является back_inserter из vector, и в этом случае он будет получать доступ к векторному объекту и резервировать достаточно места чтобы сохранить distance между заданной парой итераторов перед фактическим запуском преобразования.


person jdehesa    schedule 25.09.2018    source источник
comment
reserve твой друг здесь.   -  person NathanOliver    schedule 25.09.2018
comment
back_insert_iterator просто звонит push_back, имхо.   -  person Angelicos Phosphoros    schedule 25.09.2018
comment
Конечно можно, почему бы и нет?   -  person bipll    schedule 25.09.2018
comment
В общем случае это будет пессимизация (ломает стратегию роста и делает вставку O(n^2)). Так что лучше просто использовать reserve, чем это действительно необходимо (не часто).   -  person Dan M.    schedule 25.09.2018
comment
@ДэнМ. Я предполагаю, что это будет реализовано путем отправки тегов на основе двух типов итераторов, что означает нулевую стоимость времени выполнения.   -  person Caleth    schedule 25.09.2018
comment
@Caleth Какое отношение к этому имеет диспетчеризация тегов или типы итераторов? Если бы add3(vector&) добавил 3 элемента в вектор, разумно зарезервировав для них место, то вызов его в цикле привел бы к квадратичному поведению, поскольку емкость вектора не росла бы экспоненциально. То же самое с тем, что вы предлагаете. reserve небезопасно, и его следует избегать в любом месте, кроме 1) в функции, которая его создала 2) если вы знаете, что после этого он никогда не будет изменен.   -  person Dan M.    schedule 25.09.2018
comment
@ДэнМ. Идея здесь заключалась бы в том, чтобы иметь один вызов reserve на transform в начале алгоритма на основе расстояния между итераторами, а не несколько вызовов в цикле. Диспетчеризация тегов будет использоваться для решения во время компиляции, следует ли вызывать этот reserve. Однако не уверен, почему reserve следует считать небезопасным (или более небезопасным, чем, например, push_back).   -  person jdehesa    schedule 25.09.2018
comment
@ДэнМ. vector::reserve разрешено перераспределять. Он может сохранить свою стратегию роста, несмотря на неоднократные обращения к reserve   -  person Caleth    schedule 25.09.2018
comment
@Caleth разрешено, это не значит, что это реализовано таким образом. Полагаться на это было бы плохо.   -  person Dan M.    schedule 25.09.2018
comment
@jdehesa я объяснил почему. Или вы можете обратиться к примечаниям здесь: en.cppreference.com/w/cpp /контейнер/вектор/резерв   -  person Dan M.    schedule 25.09.2018
comment
Проблема с reserve в том, что он не является частью концепта. Это потребует очень специфической специализации, например copy(RandomAccess, RandomAccess, std::back_inserter<std::vector<T>>). Вы можете легко реализовать это; вопрос (который у меня есть) заключается в том, где именно (пространство имен) поставить такую ​​функцию. (Не уверен, что вообще разрешено писать std::).   -  person alfC    schedule 25.09.2018
comment
@ДэнМ. речь идет о гипотетической реализации стандартной библиотеки. Вам позволено зависеть от вашего собственного выбора   -  person Caleth    schedule 25.09.2018


Ответы (4)


Это потребовало бы большого количества специальных регистров в библиотеке с очень небольшой пользой.

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

Поскольку единственным преимуществом было бы вознаграждение программистов, которые слишком ленивы, чтобы вызвать reserve(), я думаю, маловероятно, что какой-либо реализатор реализовал бы такую ​​вещь. Тем более, что для его работы, вероятно, потребуется std::​distance() на входных итераторах, что еще больше ограничивает его использование.

Также обратите внимание, что такая реализация должна будет хранить ссылку на вектор-владелец в своих итераторах и не сможет использовать наиболее распространенное представление std::​vector<T>::​iterator, а именно T*. Это цена, которую придется нести всем пользователям, независимо от того, будут ли они использовать эту новую функцию.

Технически возможно? Возможно, в некоторых случаях. Разрешенный? Я так думаю. Хорошее соотношение цены и качества? Нет.

person Toby Speight    schedule 25.09.2018
comment
Да, продолжая думать над вопросом, я прихожу к тому же заключению. Если вы имеете дело с большими коллекциями, вы в любом случае должны помнить о производительности, и кажется неадекватным возлагать ответственность за оптимизацию каждого возможного варианта использования на поставщика stdlib. - person jdehesa; 25.09.2018
comment
Проблема вызвана не ленивыми программистами, а общим кодом, в котором алгоритм, а не вызывающая сторона, несет ответственность за знание о существовании reserve. Но я согласен, что это потребует очень специфической специализации, например std::copy(RandomAccess, RandomAccess, std::back_inserter<std::vector<T>>). - person alfC; 25.09.2018
comment
Обратите внимание, что это newVector, на который ссылается std::back_inserter<std::vector<int>>, а не longVector, который предоставляет std::vector<int>::iterators, на которые будет вызван reserve. Вы бы сделали трейт, чтобы различать OutputIterator, которые могут выиграть от резервирования. - person Caleth; 25.09.2018

Вы можете почти добиться желаемого эффекта от выделения 1 памяти, используя boost::transform_iterator вместо std::transform с std::back_inserter .

Однако проблема заключается в том, что, поскольку boost::transform_iterator не может вернуть ссылку на элемент, он помечен как std::input_iterator_tag. Входные итераторы являются однопроходными итераторами, в отличие от других категорий итераторов, и при передаче в конструктор диапазона std::vector он использует push_back для заполнения вектора.

Вы можете принудительно восстановить исходную категорию итератора и добиться желаемого эффекта выделения памяти 1 с оговоркой, что такой итератор нарушает стандартное требование, согласно которому двунаправленный итератор или итератор с произвольным доступом должен возвращать ссылки на элементы:

#include <boost/iterator/transform_iterator.hpp>
#include <algorithm>
#include <vector>

template<class I>
struct original_category_iterator : I {
    using iterator_category = typename std::iterator_traits<typename I::base_type>::iterator_category;
    using I::I;
};

template<class I>
inline original_category_iterator<I> original_category(I i) {
    return {i};
}

int main() {
    std::vector<int> longVector = {1,2,3};
    auto f = [](auto i) { return i * i; };
    std::vector<int> newVector(original_category(boost::make_transform_iterator(longVector.begin(), f)),
                               original_category(boost::make_transform_iterator(longVector.end(), f)));
}
person Maxim Egorushkin    schedule 25.09.2018
comment
Конструктору std::vector нужен только InputIterator. Ничего восстанавливать не надо. Ваша первая версия работает отлично. - person n. 1.8e9-where's-my-share m.; 25.09.2018
comment
@н.м. stdlibc++ векторный конструктор выполняет диспетчеризацию тегов и использует одно выделение для std::forward_iterator_tag итераторов. Я объяснил это во втором абзаце. - person Maxim Egorushkin; 25.09.2018
comment
Я понятия не имею, почему stdlibc++ стратегии реализации должны что-то делать с этим. Стандарты говорят, что векторному конструктору нужен InputIterator. Зачем stdlibc++ делать что-то особенное для других категорий итераторов? - person n. 1.8e9-where's-my-share m.; 25.09.2018
comment
@н.м. Чтобы избежать многократного выделения памяти. Стандарт говорит, что итератор должен быть итератором ввода, вы правы. Любой итератор является итератором ввода, кроме итератора вывода. - person Maxim Egorushkin; 25.09.2018
comment
Хорошо, понял, это будет работать, но не с одним выделением. - person n. 1.8e9-where's-my-share m.; 25.09.2018
comment
Спасибо за исчерпывающий ответ. Концепции итераторов всегда меня смущают, но это полезно. - person jdehesa; 26.09.2018

Хорошей новостью является то, что библиотека диапазонов делает резерв для контейнеров итераторов с произвольным доступом, поэтому, если вы хотите, вы можете использовать его.

Теперь вернемся к проблеме:

резервирование в цикле проблематично

Трудно объяснить, не читая код, но если бы алгоритм STL вызывался в цикле и он выполнял резерв это может вызвать квадратичную сложность. Проблема в том, что некоторые контейнеры STL резервируют точное количество запрошенной памяти (это понятно для небольших размеров, но для больших IMAO это неправильное поведение), например, если текущая емкость равна 1000, и вы вызываете резерв (1005), резерв (1010 ), зарезервируйте (1010), это вызовет 3 перераспределения (это означает, что вы копируете ~ 1000 элементов каждый раз, чтобы получить место для 5 дополнительных). Вот код, он немного длинный, но я надеюсь, что вы поняли идею:

#include<vector>
    #include<iostream>
    #include<chrono>
    int main(){
         std::vector<float> vec(10000,1.0f);
         std::vector<std::vector<float>> small_vecs(5000, std::vector<float>(50,2.0f));
         const auto t_start = std::chrono::high_resolution_clock::now();
         for(size_t i = 0; i < small_vecs.size(); i++) {
             // uncomment next line for quadratic complexity
             //vec.reserve(vec.size()+small_vecs[i].size());
             for (size_t j=0; j< small_vecs[i].size(); ++j){
                 vec.push_back(small_vecs[i][j]);
             }
         }
         const auto t_end = std::chrono::high_resolution_clock::now();
         std::cout << "runtime:" <<
             std::chrono::duration_cast<std::chrono::milliseconds>(t_end - t_start).count()
             << "ms\n";
    }

бонус:

в прошлый раз, когда я тестировал его, back_iterator даже с резервом был патетически медленным (замедление измеряется в x , а не в %), поэтому, если вы заботитесь о производительности, убедитесь, что при использовании back_inserter ваш код сравнивается с ручным циклом.

person NoSenseEtAl    schedule 25.09.2018
comment
Бенчмарк интересный. В принципе, я думал об одном вызове transform, но понимаю, что частые вызовы могут повредить производительности. Хотя, если я возьму на себя роль разработчика stdlib и решу реализовать эту функцию, я, вероятно, смогу использовать свою реализацию reserve лучше, чем просто несколько раз вызывать push_back. - person jdehesa; 26.09.2018

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

Как и clear() и erase(), функция backup() изменяет контейнер. Введение резерва () делает алгоритмы осведомленными о контейнерах, что идет вразрез с чистым дизайном этого резкого разделения.

Также у вас могло быть

deque<int> longDeque = { ... };
deque<int> newDeque;
transform(longDeque.begin(), longDeque.end(), back_inserter(newDeque),
          [] (int i) { return i * i; });

or

list<int> longList = { ... };
list<int> newList;
transform(longList.begin(), longList.end(), back_inserter(newList),
          [] (int i) { return i * i; });

и std::deque и std::list не поддерживают резерв(), но код тот же.

И последнее замечание: у вектора нет push_front(), поэтому front_inserter() не нужно поддерживать.

person SJHowe    schedule 26.09.2018