Возьми это
#include <range/v3/view/remove_if.hpp>
#include <range/v3/range/conversion.hpp>
#include <vector>
std::vector<int> foo(std::vector<int> v, bool(*p)(int)) {
return v | ranges::views::remove_if (p) | ranges::to_vector;
}
по сравнению с этим
#include <range/v3/action/remove_if.hpp>
#include <vector>
std::vector<int> bar(std::vector<int> v, bool(*p)(int)) {
return std::move(v) | ranges::actions::remove_if (p);
}
Никаких шаблонов, только два TU, каждый из которых предоставляет чистую функцию с одной и той же сигнатурой. Учитывая их реализацию, я ожидаю, что эти две функции выполнят одну и ту же задачу с точки зрения вызывающей стороны. И это то, что они, кажется, делают.
Однако они компилируются в довольно разный код, вплоть до того, что GCC (по крайней мере, ствол) создает более короткий код для последнего, тогда как Clang (транк) создает более короткий код для первого.
Я не вижу никакой причины компилировать две функции в разный код, кроме как «компилятору слишком сложно создать один и тот же код для обеих», но что делает это таким трудным? Или, если я ошибаюсь, почему две функции должны компилироваться в разные сборки?
И, кроме сравнительного анализа, есть ли причина, по которой я должен предпочесть одну реализацию другой?
Полный пример .
Добавьте -DNDEBUG. Вторая версия имеет утверждения. Если добавить -fno-exceptions, то получатся ассемблеры еще меньшего размера.
@3CxEZiVlQ, можешь уточнить? Я не вижу, что вы говорите.
Код ranges::to_vector кажется короче только потому, что вы отфильтровали весь код в проводнике компилятора: godbolt.org/z/fcY8f6ezd
@Artyer, я не вижу никаких изменений в опциях, так что же это за «отфильтрованное»?
@Enlico Filter > Библиотечные функции
@Артьер, почему бы мне не фильтровать? Я просто хочу, чтобы объектный файл содержал функцию foo/bar, не так ли? Или я неправильно понимаю смысл фильтрации в данном случае?
@Enlico Генератор clang, который, как вы утверждаете, короче, содержит вызов ranges::detail::to_container::fn. Это необходимо выполнить для запуска foo, но в вашем примере это не показано.
@Artyer, я не понимаю, Range-v3 - это библиотека только заголовков, так почему же компилятор выбрасывает ее части? Это не значит, что foo вызывает что-то, определение которого будет найдено компоновщиком в другом TU...
Возможно багажник не справляется -DNDEBUG.
@Enlico Это веб-сайт компилятора-проводника, отфильтровывающий (т. е. не отображающий, он все еще там) код, сгенерированный из шаблонов в диапазоне-v3.
Проще говоря, хотя результаты в некоторой степени эквивалентны, способы их достижения различны, а также результаты не равны, поскольку первая версия обязательно будет выделять новые данные, а вторая - нет, что будет иметь существенное значение, если внешний код предполагает, что перераспределения не происходит. Это также может повлиять на различные оптимизации, которые может выполнить компилятор.





Я не уверен, что даже теоретически возможно, что они сгенерируют один и тот же код. Давайте рассмотрим два подхода.
std::vector<int> bar(std::vector<int> v, bool(*p)(int)) {
return std::move(v) | ranges::actions::remove_if (p);
}
С помощью действий это берет v, мутирует его на месте, чтобы удалить элементы, удовлетворяющие p, и возвращает то же самое v обратно. Это эквивалентно написанию:
std::vector<int> bar(std::vector<int> v, bool(*p)(int)) {
std::erase_if (v, p);
return v;
}
Или, как было до C++20:
std::vector<int> bar(std::vector<int> v, bool(*p)(int)) {
v.erase(std::remove_if (v.begin(), v.end(), p), v.end());
return v;
}
Определенно никакого распределения не происходит, мы просто перемещаем кучу int, а затем меняем v.size().
std::vector<int> foo(std::vector<int> v, bool(*p)(int)) {
return v | ranges::views::remove_if (p) | ranges::to_vector;
}
views::remove_if — ленивый фильтр. Это дает нам представление об элементах v, которые не удовлетворяют p. Затем to_vector собирается построить новый vector, который требует выделения, и скопировать все элементы v, которые не удовлетворяют p, в новый vector. Этот новый вектор возвращается.
Первоначально выражение v | remove_if (p) | to_vector выделяет новый vector<int>, отличный от v. v жив на протяжении всего этого выражения, поэтому вы не можете повторно использовать здесь память v.
Оптимизация здесь заключается не только в том, чтобы признать, что v неизбежно уничтожается, и поэтому его выделение можно использовать повторно. Но также новый vector имеет не более того же размера, что и v, поэтому повторное использование его выделения является жизнеспособной стратегией. Но также и то, что элементы этого нового vector заполняются таким образом, чтобы можно было повторно использовать это распределение.
По сути, эти два случая представляют собой просто разные алгоритмы. Иногда компиляторы могут это понять, но это кажется огромной натяжкой. Если бы такая оптимизация существовала, она бы, по сути, была создана вручную для этого сценария.
И что мне предпочесть и почему?
В общем, ответом на этот вопрос будет использование наиболее конкретного инструмента для работы. Если у вас есть vector<int> и вам просто нужны элементы, которые не удовлетворяют p, и вам вообще не нужны исходные элементы - это actions::remove_if (или, в зависимости от контекста, просто прямой вызов std::erase_if). Это задача, для решения которой создан actions::remove_if.
Если вам не нужен контейнер всех элементов, удовлетворяющих p, а нужно просто по требованию выбрать (некоторые) из них — это views::remove_if.
Иногда желание лучше. Иногда лучше лениться. Это действительно зависит от проблемы.
ranges::to_vectorвыделит новый вектор во время вызова, в первом примере используется выделенный вектор вызывающего абонента. Он использует оптимизацию возвращаемого значения, поэтому код выглядит по-другому. Единственный реальный способ узнать, какой из них имеет более высокую производительность, — это профилировать сгенерированный код. Также обратите внимание, что дополнительный ассемблерный код не преобразует 1-1 в более медленный код (это может быть даже быстрее, если это означает, что ЦП может лучше использовать свою конвейерную обработку).