Почему диапазоны::views::remove_if | ranges::to_vector и ranges::actions::remove_if генерируют другой код? И что мне предпочесть и почему?

Возьми это

#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 (транк) создает более короткий код для первого.

Я не вижу никакой причины компилировать две функции в разный код, кроме как «компилятору слишком сложно создать один и тот же код для обеих», но что делает это таким трудным? Или, если я ошибаюсь, почему две функции должны компилироваться в разные сборки?

И, кроме сравнительного анализа, есть ли причина, по которой я должен предпочесть одну реализацию другой?

Полный пример .

ranges::to_vector выделит новый вектор во время вызова, в первом примере используется выделенный вектор вызывающего абонента. Он использует оптимизацию возвращаемого значения, поэтому код выглядит по-другому. Единственный реальный способ узнать, какой из них имеет более высокую производительность, — это профилировать сгенерированный код. Также обратите внимание, что дополнительный ассемблерный код не преобразует 1-1 в более медленный код (это может быть даже быстрее, если это означает, что ЦП может лучше использовать свою конвейерную обработку).
Pepijn Kramer 02.08.2024 08:48

Добавьте -DNDEBUG. Вторая версия имеет утверждения. Если добавить -fno-exceptions, то получатся ассемблеры еще меньшего размера.

3CxEZiVlQ 02.08.2024 08:53

@3CxEZiVlQ, можешь уточнить? Я не вижу, что вы говорите.

Enlico 02.08.2024 09:00

Код ranges::to_vector кажется короче только потому, что вы отфильтровали весь код в проводнике компилятора: godbolt.org/z/fcY8f6ezd

Artyer 02.08.2024 09:03

@Artyer, я не вижу никаких изменений в опциях, так что же это за «отфильтрованное»?

Enlico 02.08.2024 09:07

@Enlico Filter > Библиотечные функции

Artyer 02.08.2024 09:09

@Артьер, почему бы мне не фильтровать? Я просто хочу, чтобы объектный файл содержал функцию foo/bar, не так ли? Или я неправильно понимаю смысл фильтрации в данном случае?

Enlico 02.08.2024 09:11

@Enlico Генератор clang, который, как вы утверждаете, короче, содержит вызов ranges::detail::to_container::fn. Это необходимо выполнить для запуска foo, но в вашем примере это не показано.

Artyer 02.08.2024 09:15

@Artyer, я не понимаю, Range-v3 - это библиотека только заголовков, так почему же компилятор выбрасывает ее части? Это не значит, что foo вызывает что-то, определение которого будет найдено компоновщиком в другом TU...

Enlico 02.08.2024 09:26

Возможно багажник не справляется -DNDEBUG.

3CxEZiVlQ 02.08.2024 09:28

@Enlico Это веб-сайт компилятора-проводника, отфильтровывающий (т. е. не отображающий, он все еще там) код, сгенерированный из шаблонов в диапазоне-v3.

Artyer 02.08.2024 09:31

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

ALX23z 02.08.2024 10:48
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
12
78
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я не уверен, что даже теоретически возможно, что они сгенерируют один и тот же код. Давайте рассмотрим два подхода.

Действия

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.

Иногда желание лучше. Иногда лучше лениться. Это действительно зависит от проблемы.

Другие вопросы по теме