Будут ли компиляторы C++ оптимизировать функцию «идентичности»?

Я хочу написать код, который применяет к строкам различные преобразования. Одним из преобразований на самом деле является noop:

std::string transform(const std::string& in) {
  return in;
}

Поймет ли компилятор, что это ошибка, и оптимизирует ее? Другими словами, есть ли разница между этим

int main() {
  std::cout << "test";
}

и это:

int main() {
  std::cout << transform("test");
}

когда transform определяется, как указано выше? может ли компилятор скомпилировать их в один и тот же код (ассемблер?)?

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

Some programmer dude 26.06.2024 02:31

Это действительно зависит от (качества реализации) вашего компилятора (или, точнее, набора инструментов). Даже если функция явно указана inline (например, ее определение в заголовке отмечено inline), компилятору разрешено решить не встраивать ее. Точно так же, когда функция явно не является встроенной (например, определение в отдельном исходном файле), реализация может свободно встроить ее (хотя при использовании цепочки инструментов компиляции компоновщик часто решает, следует ли встраивать функцию, а не компилятор). . Стандарт C++ предоставляет в этом отношении большую свободу реализации.

Peter 26.06.2024 03:39

Если transform не виден компилятору, например, он определен в другой единице перевода и все, что может видеть текущая единица перевода, — это объявление в заголовке, она, вероятно, не сможет выполнить эту оптимизацию. Но особенно умный компоновщик может это сделать.

user4581301 26.06.2024 03:56

Компилятор может встроить, но не может оптимизировать вашу функцию transform, поскольку она возвращает значение r и принимает ссылку на значение lvalue. Компилятор так или иначе создает временную строку, вопрос только в том, будет ли вызов или немедленная реализация.

Gene 26.06.2024 04:36

это не личность, а копия

463035818_is_not_an_ai 26.06.2024 06:44

функция не является нет. Это копия, которая представляет собой потенциально дорогостоящую операцию.

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

Ответы 1

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

Это зависит. Давайте возьмем этот код в качестве примера.

#include <iostream>

std::string transform(std::string const& in)
{
  return in;
}

int main(void)
{
  std::cout << transform("Hello World!");
}

Используя Clang/LLVM, мы получаем это.

define dso_local void @transform(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)(ptr dead_on_unwind noalias writable sret(%"class.std::__cxx11::basic_string") align 8 %0, ptr noundef nonnull align 8 dereferenceable(32) %1) #0 !dbg !1559 {
  %3 = alloca ptr, align 8
  %4 = alloca ptr, align 8
  store ptr %0, ptr %3, align 8
  store ptr %1, ptr %4, align 8
    #dbg_declare(ptr %4, !1566, !DIExpression(), !1567)
  %5 = load ptr, ptr %4, align 8, !dbg !1568
  call void @std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)(ptr noundef nonnull align 8 dereferenceable(32) %0, ptr noundef nonnull align 8 dereferenceable(32) %5), !dbg !1568
  ret void, !dbg !1569
}

define dso_local noundef i32 @main() #2 personality ptr @__gxx_personality_v0 !dbg !1570 {
  %1 = alloca ptr, align 8
  %2 = alloca ptr, align 8
  %3 = alloca ptr, align 8
  %4 = alloca ptr, align 8
  %5 = alloca %"class.std::__cxx11::basic_string", align 8
  %6 = alloca %"class.std::__cxx11::basic_string", align 8
  %7 = alloca %"class.std::allocator", align 1
  %8 = alloca ptr, align 8
  %9 = alloca i32, align 4
  store ptr %7, ptr %4, align 8
    #dbg_declare(ptr %4, !1571, !DIExpression(), !1574)
  %10 = load ptr, ptr %4, align 8
  store ptr %10, ptr %1, align 8
    #dbg_declare(ptr %1, !1576, !DIExpression(), !1579)
  %11 = load ptr, ptr %1, align 8
  invoke void @std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string<std::allocator<char>>(char const*, std::allocator<char> const&)(ptr noundef nonnull align 8 dereferenceable(32) %6, ptr noundef @.str, ptr noundef nonnull align 1 dereferenceable(1) %7)
          to label %12 unwind label %17, !dbg !1581

12:
  invoke void @transform(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)(ptr dead_on_unwind writable sret(%"class.std::__cxx11::basic_string") align 8 %5, ptr noundef nonnull align 8 dereferenceable(32) %6)
          to label %13 unwind label %21, !dbg !1582

13:
  %14 = invoke noundef nonnull align 8 dereferenceable(8) ptr @std::basic_ostream<char, std::char_traits<char>>& std::operator<<<char, std::char_traits<char>, std::allocator<char>>(std::basic_ostream<char, std::char_traits<char>>&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)(ptr noundef nonnull align 8 dereferenceable(8) @std::cout, ptr noundef nonnull align 8 dereferenceable(32) %5)
          to label %15 unwind label %25, !dbg !1583

15:
  call void @std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string()(ptr noundef nonnull align 8 dereferenceable(32) %5) #7, !dbg !1584
  call void @std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string()(ptr noundef nonnull align 8 dereferenceable(32) %6) #7, !dbg !1584
  store ptr %7, ptr %3, align 8
    #dbg_declare(ptr %3, !1585, !DIExpression(), !1587)
  %16 = load ptr, ptr %3, align 8
  call void @std::__new_allocator<char>::~__new_allocator()(ptr noundef nonnull align 1 dereferenceable(1) %16) #7, !dbg !1589
  ret i32 0, !dbg !1591

17:
  %18 = landingpad { ptr, i32 }
          cleanup, !dbg !1591
  %19 = extractvalue { ptr, i32 } %18, 0, !dbg !1591
  store ptr %19, ptr %8, align 8, !dbg !1591
  %20 = extractvalue { ptr, i32 } %18, 1, !dbg !1591
  store i32 %20, ptr %9, align 4, !dbg !1591
  br label %30, !dbg !1591

21:
  %22 = landingpad { ptr, i32 }
          cleanup, !dbg !1591
  %23 = extractvalue { ptr, i32 } %22, 0, !dbg !1591
  store ptr %23, ptr %8, align 8, !dbg !1591
  %24 = extractvalue { ptr, i32 } %22, 1, !dbg !1591
  store i32 %24, ptr %9, align 4, !dbg !1591
  br label %29, !dbg !1591

25:
  %26 = landingpad { ptr, i32 }
          cleanup, !dbg !1591
  %27 = extractvalue { ptr, i32 } %26, 0, !dbg !1591
  store ptr %27, ptr %8, align 8, !dbg !1591
  %28 = extractvalue { ptr, i32 } %26, 1, !dbg !1591
  store i32 %28, ptr %9, align 4, !dbg !1591
  call void @std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string()(ptr noundef nonnull align 8 dereferenceable(32) %5) #7, !dbg !1584
  br label %29, !dbg !1584

29:
  call void @std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string()(ptr noundef nonnull align 8 dereferenceable(32) %6) #7, !dbg !1584
  br label %30, !dbg !1584

30:
  store ptr %7, ptr %2, align 8
    #dbg_declare(ptr %2, !1585, !DIExpression(), !1592)
  %31 = load ptr, ptr %2, align 8
  call void @std::__new_allocator<char>::~__new_allocator()(ptr noundef nonnull align 1 dereferenceable(1) %31) #7, !dbg !1594
  br label %32, !dbg !1584

32:
  %33 = load ptr, ptr %8, align 8, !dbg !1584
  %34 = load i32, ptr %9, align 4, !dbg !1584
  %35 = insertvalue { ptr, i32 } poison, ptr %33, 0, !dbg !1584
  %36 = insertvalue { ptr, i32 } %35, i32 %34, 1, !dbg !1584
  resume { ptr, i32 } %36, !dbg !1584
}

Эта сборка (технически LLIR, но я не буду вдаваться в подробности) очень нечитабельна, но вот ее псевдокод.

function transform(string: pointer): void
{
  let out: pointer = std::string::new(string);
  return out;
}

function main(): int32
{
  let cstring: pointer = "Hello World!"
  let string: pointer = std::string::new(cstring);
  let out: pointer = call transform(string);
}

По сути, «преобразование» — это всего лишь тонкая оболочка поверх конструктора копирования строк (std::string::string(std::string const &string)), и она исчезнет, ​​когда вы включите оптимизацию. Однако что не исчезнет, ​​так это вызов конструктора копирования. Дело в том, что, поскольку этот конструктор копирования уже скомпилирован (поскольку экземпляр std::basic_string<char> уже был создан ранее), ваш компилятор не может быть уверен, имеет ли конструктор копирования побочные эффекты (например, он может печатать на экране), поэтому он может не просто пропустить это.

Решением этой проблемы является перекомпиляция стандартной библиотеки C++ вместе с вашим кодом. Очевидно, это смешно. Создание копии строки — настолько дешевая операция, что, если ваша строка не является целым файлом, я бы вообще не беспокоился. В противном случае вы можете выбрать более быстрые варианты (например, необработанный char* или std::shared_ptr<std::string>), чтобы делать что-то по ссылке, а не по значению, что сводит к минимуму построение копирования. Если вы используете std::shared_ptr, обязательно проходите мимо const&, когда это возможно, чтобы избежать вызовов retain/reduce (управление памятью ARC). В противном случае вы можете просто использовать std::unique_ptr, который гарантирует, что вы всегда делаете это, и экономит ~ 8 байт (зависит от реализации) памяти подсчета ссылок, если вы выполняете кучу строковых операций.

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

Эй, спасибо за подробное объяснение. Почему вообще вызывается конструктор копирования? Не будет ли передача по ссылке + оптимизация исключения копирования/возвращаемого значения избегать вызова конструктора копирования?

sencer 26.06.2024 03:45

@sencer По сути, конструктор копирования уже скомпилирован. Все, что знает компилятор, это то, что он принимает std::string и ничего не возвращает. Это может иметь побочные эффекты (например, рисование на экране), а может и не иметь. Таким образом, компилятор вызывает его, хотя знает, что это бессмысленно. По той же причине он не оптимизирует вызовы std::cout << "...", даже если вы не используете возвращаемое значение; у него есть побочные эффекты. Конструктор копирования вызывается в операторе return как часть преобразования типа из const& в значение. Если вы вернете const&, этого не произойдет.

Spencer Rosas-Gunn 26.06.2024 04:04

Ах, большое спасибо! Думаю, ваши последние два предложения были той частью, которую я искал; с этим и остальное тоже несколько щелкнуло. ... но компилятор может выполнять копирование в другое время, почему тогда он не беспокоится о том, что реализация копирования будет иметь побочные эффекты? Кроме того, я предполагаю, что изменение преобразования для приема строки вместо строки& - ужасная идея, но я не совсем понимаю, почему это не сработает в этом конкретном случае

sencer 26.06.2024 04:55

«Не будет ли передача по ссылке + оптимизация исключения копирования/возвращаемого значения избегать вызова конструктора копирования?» Вы не можете RVO вернуть параметр. "copy elision" допускается в следующих случаях: в операторе возврата в функции с типом возврата класса, когда выражение является именем энергонезависимого объекта с автоматическим сроком хранения (отличным от параметра функции или. ..) (выделено мной)

Raymond Chen 26.06.2024 05:23

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