Я хочу написать код, который применяет к строкам различные преобразования. Одним из преобразований на самом деле является noop:
std::string transform(const std::string& in) {
return in;
}
Поймет ли компилятор, что это ошибка, и оптимизирует ее? Другими словами, есть ли разница между этим
int main() {
std::cout << "test";
}
и это:
int main() {
std::cout << transform("test");
}
когда transform
определяется, как указано выше? может ли компилятор скомпилировать их в один и тот же код (ассемблер?)?
Это действительно зависит от (качества реализации) вашего компилятора (или, точнее, набора инструментов). Даже если функция явно указана inline
(например, ее определение в заголовке отмечено inline
), компилятору разрешено решить не встраивать ее. Точно так же, когда функция явно не является встроенной (например, определение в отдельном исходном файле), реализация может свободно встроить ее (хотя при использовании цепочки инструментов компиляции компоновщик часто решает, следует ли встраивать функцию, а не компилятор). . Стандарт C++ предоставляет в этом отношении большую свободу реализации.
Если transform
не виден компилятору, например, он определен в другой единице перевода и все, что может видеть текущая единица перевода, — это объявление в заголовке, она, вероятно, не сможет выполнить эту оптимизацию. Но особенно умный компоновщик может это сделать.
Компилятор может встроить, но не может оптимизировать вашу функцию transform
, поскольку она возвращает значение r и принимает ссылку на значение lvalue. Компилятор так или иначе создает временную строку, вопрос только в том, будет ли вызов или немедленная реализация.
это не личность, а копия
функция не является нет. Это копия, которая представляет собой потенциально дорогостоящую операцию.
Это зависит. Давайте возьмем этот код в качестве примера.
#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 По сути, конструктор копирования уже скомпилирован. Все, что знает компилятор, это то, что он принимает std::string
и ничего не возвращает. Это может иметь побочные эффекты (например, рисование на экране), а может и не иметь. Таким образом, компилятор вызывает его, хотя знает, что это бессмысленно. По той же причине он не оптимизирует вызовы std::cout << "..."
, даже если вы не используете возвращаемое значение; у него есть побочные эффекты. Конструктор копирования вызывается в операторе return как часть преобразования типа из const&
в значение. Если вы вернете const&
, этого не произойдет.
Ах, большое спасибо! Думаю, ваши последние два предложения были той частью, которую я искал; с этим и остальное тоже несколько щелкнуло. ... но компилятор может выполнять копирование в другое время, почему тогда он не беспокоится о том, что реализация копирования будет иметь побочные эффекты? Кроме того, я предполагаю, что изменение преобразования для приема строки вместо строки& - ужасная идея, но я не совсем понимаю, почему это не сработает в этом конкретном случае
«Не будет ли передача по ссылке + оптимизация исключения копирования/возвращаемого значения избегать вызова конструктора копирования?» Вы не можете RVO вернуть параметр. "copy elision" допускается в следующих случаях: в операторе возврата в функции с типом возврата класса, когда выражение является именем энергонезависимого объекта с автоматическим сроком хранения (отличным от параметра функции или. ..) (выделено мной)
Правильный минимальный воспроизводимый пример может помочь нам лучше понять вашу проблему. Также расскажите нам, как именно вы строите.