Каков наиболее эффективный способ итерации std :: vector и почему?
С точки зрения пространственно-временной сложности, какой из следующих способов является лучшим способом перебора std :: vector и почему?
Способ 1:
for(std::vector<T>::iterator it = v.begin(); it != v.end(); ++it) {
/* std::cout << *it; ... */
}
Способ 2:
for(std::vector<int>::size_type i = 0; i != v.size(); i++) {
/* std::cout << v[i]; ... */
}
Способ 3:
for(size_t i = 0; i != v.size(); i++) {
/* std::cout << v[i]; ... */
}
Способ 4:
for(auto const& value: a) {
/* std::cout << value; ... */
Ответы
Ответ 1
Прежде всего, путь 2 и путь 3 идентичны практически во всех стандартных реализациях библиотеки.
Кроме того, опции, которые вы разместили, практически эквивалентны. Единственное заметное отличие состоит в том, что в Пути 1 и Пути 2/3 вы полагаетесь на компилятор, чтобы оптимизировать вызов v.end()
и v.size()
out. Если это предположение верно, то между циклами нет разницы в производительности.
Если это не так, способ 4 является наиболее эффективным. Вспомните, как диапазон, основанный на цикле, расширяется до
{
auto && __range = range_expression ;
auto __begin = begin_expr ;
auto __end = end_expr ;
for ( ; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
Важной частью здесь является то, что это гарантирует, что end_expr
будет оцениваться только один раз. Также обратите внимание, что для того, чтобы диапазон, основанный на цикле, был наиболее эффективной итерацией, вы не должны изменять способ обработки разыменования итератора, например
for (auto value: a) { /* ... */ }
это копирует каждый элемент вектора в value
переменной цикла, которое, вероятно, будет медленнее, чем for (const auto& value: a)
, в зависимости от размера элементов в векторе.
Обратите внимание, что с помощью средств параллельного алгоритма в С++ 17 вы также можете попробовать
#include <algorithm>
#include <execution>
std::for_each(std::par_unseq, a.cbegin(), a.cend(),
[](const auto& e) { /* do stuff... */ });
но будет ли это быстрее, чем обычная петля, зависит от возможных подробностей.
Ответ 2
Дополнение к lubgr ответу:
Если вы не обнаружите с помощью профилирования рассматриваемого кода как узкое место, эффективность (которую вы, вероятно, имели в виду вместо "эффективности") не должна быть вашей первой заботой, по крайней мере, на этом уровне кода. Гораздо важнее читаемость кода и удобство сопровождения! Таким образом, вы должны выбрать вариант цикла, который читается лучше всего, что обычно является способом 4.
Индексы могут быть полезны, если у вас есть шаги больше 1 (почему бы вам ни понадобилось...):
for(size_t i = 0; i < v.size(); i += 2) { ... }
Хотя += 2
само по себе допустимо и для итераторов, вы рискуете неопределенным поведением в конце цикла, если вектор имеет нечетный размер, потому что вы увеличиваете на единицу после конечной позиции! (Обычно говорят: если вы увеличиваете на n, вы получаете UB, если размер не является точным кратным n.) Поэтому вам нужен дополнительный код, чтобы поймать это, в то время как у вас нет варианта индекса...
Ответ 3
Предпочитаю итераторы индексам/ключам.
В то время как для vector
или array
не должно быть никакой разницы между любой формой 1 это хорошая привычка для других контейнеров.
1 Если, конечно, вы используете []
вместо .at()
для доступа по индексу.
Запомни конечную границу.
Повторное вычисление конечной границы на каждой итерации неэффективно по двум причинам:
- В общем: локальная переменная не имеет псевдонимов, что более удобно для оптимизатора.
- На контейнерах, отличных от vector: вычисление end/size может быть немного дороже.
Вы можете сделать это как однострочник:
for (auto it = vec.begin(), end = vec.end(); it != end; ++it) { ... }
(Это исключение из общего запрета на объявление одной переменной за раз.)
Используйте форму для каждого цикла.
Форма для каждого цикла автоматически:
- Используйте итераторы.
- Запомни конечную границу.
Таким образом:
for (/*...*/ value : vec) { ... }
Возьмите встроенные типы по значениям, другие типы по ссылке.
Существует неочевидный компромисс между взятием элемента по значению и взятием элемента по ссылке:
- Взятие элемента по ссылке позволяет избежать копирования, что может быть дорогостоящей операцией.
- Взятие элемента по значению более удобно для оптимизатора 1.
В крайнем случае выбор должен быть очевиден:
- Встроенные типы (
int
, std::int64_t
, void*
,...) должны быть приняты по значению. - Потенциально выделяемые типы (
std::string
,...) должны быть взяты по ссылке.
В середине, или когда сталкиваешься с общим кодом, я бы рекомендовал начинать со ссылок: лучше избегать снижения производительности, чем пытаться выжать последний цикл.
Таким образом, общая форма:
for (auto& element : vec) { ... }
И если вы имеете дело со встроенным:
for (int element : vec) { ... }
1 Это общий принцип оптимизации, на самом деле: локальные переменные являются дружественными, чем указателей/ссылок, поскольку оптимизатор знает все возможные псевдонимы (или отсутствие, их) локальной переменной.
Ответ 4
Ленивый ответ: Сложности эквивалентны.
- Временная сложность всех решений Θ (n).
- Пространственная сложность всех решений Θ (1).
Постоянные факторы, вовлеченные в различные решения, являются деталями реализации. Если вам нужны цифры, вам лучше всего сравнить различные решения в вашей конкретной целевой системе.
Это может помочь сохранить v.size()
rsp. v.end()
, хотя они обычно встроены, поэтому такие оптимизации могут не потребоваться или выполняться автоматически.
Обратите внимание, что индексирование (без напоминания v.size()
) - единственный способ правильно работать с телом цикла, которое может добавлять дополнительные элементы (используя push_back()
). Тем не менее, большинство вариантов использования не требуют такой дополнительной гибкости.
Ответ 5
Для полноты я хотел бы упомянуть, что ваш цикл может захотеть изменить размер вектора.
std::vector<int> v = get_some_data();
for (std::size_t i=0; i<v.size(); ++i)
{
int x = some_function(v[i]);
if(x) v.push_back(x);
}
В таком примере вы должны использовать индексы и переоценивать v.size()
на каждой итерации.
Если вы сделаете то же самое с циклом for на основе диапазона или с итераторами, вы можете столкнуться с неопределенным поведением, поскольку добавление новых элементов в вектор может сделать ваши итераторы недействительными.
Кстати, я предпочитаю использовать while
-loops для таких случаев, а for
-loops, но это другая история.
Ответ 6
Это в значительной степени зависит от того, что вы подразумеваете под "эффективным".
В других ответах упоминалась эффективность, но я собираюсь сосредоточиться на (IMO) наиболее важной цели кода C++: донести свое намерение до других программистов¹.
С этой точки зрения метод 4, безусловно, является наиболее эффективным. Не только потому, что читается меньше символов, но главным образом потому, что меньше когнитивная нагрузка: нам не нужно проверять, необычны ли границы или размер шага, используется ли переменная итерации цикла (i
или it
) или изменена где-либо еще есть ли ошибка for (auto я = 0u; я < v1.size(); ++i) { std::cout << v2[i]; }
или копировании/вставке, например, for (auto я = 0u; я < v1.size(); ++i) { std::cout << v2[i]; }
for (auto я = 0u; я < v1.size(); ++i) { std::cout << v2[i]; }
или десятки других возможностей.
Быстрый тест: заданный std::vector<int> v1, v2, v3;
Сколько из следующих циклов являются правильными?
for (auto it = v1.cbegin(); it != v1.end(); ++it)
{
std::cout << v1[i];
}
for (auto i = 0u; i < v2.size(); ++i)
{
std::cout << v1[i];
}
for (auto const i: v3)
{
std::cout << i;
}
Выражение управления циклами настолько четко, насколько это возможно, позволяет разработчику больше понимать логику высокого уровня, а не загромождать детали реализации - в конце концов, именно поэтому мы используем C++ в первую очередь!
¹ Для ясности, когда я пишу код, я считаю, что самым важным "другим программистом" является Future Me, пытающийся понять: "Кто написал этот мусор?"...
Ответ 7
Предпочитайте метод 4, std :: for_each (если вам действительно нужно) или метод 5/6:
void method5(std::vector<float>& v) {
for(std::vector<float>::iterator it = v.begin(), e = v.end(); it != e; ++it) {
*it *= *it;
}
}
void method6(std::vector<float>& v) {
auto ptr = v.data();
for(std::size_t i = 0, n = v.size(); i != n; i++) {
ptr[i] *= ptr[i];
}
}
Первые 3 метода могут страдать от проблем с наложением указателей (как упоминалось в предыдущих ответах), но все они одинаково плохие. Учитывая, что возможно, что другой поток может обращаться к вектору, большинство компиляторов будут воспроизводить его безопасно и переоценивать [] end() и size() в каждой итерации. Это предотвратит все оптимизации SIMD.
Вы можете увидеть доказательство здесь:
https://godbolt.org/z/BchhmU
Вы заметите, что только 4/5/6 используют инструкции vmulps SIMD, где как 1/2/3 всегда используется инструкция vmulss без SIMD.
Примечание: я использую VC++ в ссылке Godbolt, потому что она хорошо демонстрирует проблему. Та же проблема возникает с gcc/clang, но продемонстрировать это с помощью godbolt нелегко - обычно вам нужно разобрать DSO, чтобы увидеть, как это происходит.
Ответ 8
Все перечисленные вами способы имеют одинаковую сложность времени и одинаковую сложность пространства (что неудивительно).
Использование синтаксиса for(auto& value: v)
несколько более эффективно, потому что с другими методами компилятор может перезагружать v.size()
и v.end()
из памяти каждый раз, когда вы выполняете тест, тогда как с помощью for(auto& value: v)
этого никогда не происходит (он загружает итераторы begin()
и end()
только один раз).
Мы можем наблюдать сравнение сборки, произведенной каждым методом здесь: https://godbolt.org/z/LnJF6p
На несколько забавном замечании компилятор реализует method3
как инструкцию jmp
для method2
.
Ответ 9
Сложность одинакова для всех, кроме последнего, который теоретически быстрее, потому что конец контейнера оценивается только один раз.
Последний также лучше всего читать и писать, но у него есть недостаток, который не дает вам индекс (что довольно часто важно).
Однако вы игнорируете то, что я считаю хорошей альтернативой (это мой предпочтительный вариант, когда мне нужен индекс, и вы не можете использовать for (auto& x: v) {...}
):
for (int i=0,n=v.size(); i<n; i++) {
... use v[i] ...
}
обратите внимание, что я использовал int
а не size_t
и что конец вычисляется только один раз, а также доступен в теле как локальная переменная.
Часто, когда нужны индекс и размер, для них также выполняются математические вычисления, и size_t
ведет себя "странно", когда используется для математики (например, a+1 < b
и a < b-1
- разные вещи).
Ответ 10
Для строки, которая либо
"Твоя криптокитница голодна. Если ты ее не покормишь, она умрет".
а также
"Твой криптокит голоден. Если ты его не накормишь, он умрет".
Как мы кодируем их в формате ресурсов Android в формате XML?