Русский
Русский
English
Статистика
Реклама

Перевод Печальная правда о пропуске копий в C



Пропуск копий (copy elision) это оптимизация компилятора, которая, как и следует из имени, устраняет лишние операции копирования и перемещения. Она аналогична классической оптимизации размножения копий, но выполняется конкретно для объектов C++, которые могут иметь нестандартные конструкторы копирования и перемещения. В этой статьей я продемонстрирую пример, в котором очевидная ожидаемая от компилятора оптимизация на практике не происходит.

Ввод дополнительной переменной для разрыва строки


Предположим, что у нас есть длинный вызов функции, возвращающий объект, который нужно мгновенно передать другой функции так:

#include <string>#include <string_view>// Тип данных, который дорого копировать, непросто удалить и невозможно переместитьstruct Widget {  std::string s;};void consume(Widget w);Widget doSomeVeryComplicatedThingWithSeveralArguments(  int arg1, std::string_view arg2);void someFunction() {    consume(doSomeVeryComplicatedThingWithSeveralArguments(123, "hello"));}

Как видно из сгенерированного кода ассемблера, здесь все отлично:

someFunction():                      # @someFunction()        pushq   %rbx        subq    $32, %rsp        movq    %rsp, %rbx        movl    $5, %edx        movl    $.L.str, %ecx        movq    %rbx, %rdi        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        movq    %rbx, %rdi        callq   consume(Widget)        movq    (%rsp), %rdi        leaq    16(%rsp), %rax        cmpq    %rax, %rdi        je      .LBB0_2        callq   operator delete(void*).LBB0_2:        addq    $32, %rsp        popq    %rbx        retq.L.str:        .asciz  "hello"

Временный Widget, возвращаемый из doSomeVeryComplicatedThingWithSeveralArguments, создается в области стека, которую под него выделила someFunction. Затем, как объяснялось в статье о правилах передачи параметров (англ.), указатель на эту область стека передается напрямую для использования.

Теперь представьте, что строка функции someFuncton показалась вам слишком длинной, или что вы хотите дать результату doSomeVeryComplicatedThingWithSeveralArguments описательное имя, для чего меняете код:

void someFunctionV2() {    auto complicatedThingResult =        doSomeVeryComplicatedThingWithSeveralArguments(123, "hello");    consume(complicatedThingResult);}

Естественно, все съезжает:

someFunctionV2():                    # @someFunctionV2()        pushq   %r15        pushq   %r14        pushq   %r12        pushq   %rbx        subq    $72, %rsp        leaq    40(%rsp), %rdi        movl    $5, %edx        movl    $.L.str, %ecx        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        leaq    24(%rsp), %r12        movq    %r12, 8(%rsp)        movq    40(%rsp), %r14        movq    48(%rsp), %rbx        movq    %r12, %r15        cmpq    $16, %rbx        jb      .LBB1_4        testq   %rbx, %rbx        js      .LBB1_13        movq    %rbx, %rdi        incq    %rdi        js      .LBB1_14        callq   operator new(unsigned long)        movq    %rax, %r15        movq    %rax, 8(%rsp)        movq    %rbx, 24(%rsp).LBB1_4:        testq   %rbx, %rbx        je      .LBB1_8        cmpq    $1, %rbx        jne     .LBB1_7        movb    (%r14), %al        movb    %al, (%r15)        jmp     .LBB1_8.LBB1_7:        movq    %r15, %rdi        movq    %r14, %rsi        movq    %rbx, %rdx        callq   memcpy.LBB1_8:        movq    %rbx, 16(%rsp)        movb    $0, (%r15,%rbx)        leaq    8(%rsp), %rdi        callq   consume(Widget)        movq    8(%rsp), %rdi        cmpq    %r12, %rdi        je      .LBB1_10        callq   operator delete(void*).LBB1_10:        movq    40(%rsp), %rdi        leaq    56(%rsp), %rax        cmpq    %rax, %rdi        je      .LBB1_12        callq   operator delete(void*).LBB1_12:        addq    $72, %rsp        popq    %rbx        popq    %r12        popq    %r14        popq    %r15        retq.LBB1_13:        movl    $.L.str.2, %edi        callq   std::__throw_length_error(char const*).LBB1_14:        callq   std::__throw_bad_alloc().L.str:        .asciz  "hello".L.str.2:        .asciz  "basic_string::_M_create"

Теперь берем наш идеальный Widget, complicatedThingResult, и копируем его в новый временный Widget, который будет передаваться в качестве первого аргумента. По завершении всех действий нужно будет удалить два Widget: complicatedThingResult и безымянный временный Widget, который мы передавали для использования. Вы можете ожидать, что компилятор оптимизирует someFunctionV2(), сделав ее подобной someFunction, но этого не произойдет.

Проблема, конечно же, в том, что мы забыли выполнить std::move complicatedThingResult:

void someFunctionV3() {    auto complicatedThingResult =        doSomeVeryComplicatedThingWithSeveralArguments(123, "hello");    consume(std::move(complicatedThingResult));}

И теперь сгенерированный код ассемблера выглядит в точности, как наш исходный пример. Постойте-качто?

someFunctionV3():                    # @someFunctionV3()        pushq   %r14        pushq   %rbx        subq    $72, %rsp        leaq    8(%rsp), %rdi        movl    $5, %edx        movl    $.L.str, %ecx        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        leaq    56(%rsp), %r14        movq    %r14, 40(%rsp)        movq    8(%rsp), %rax        leaq    24(%rsp), %rbx        cmpq    %rbx, %rax        je      .LBB1_1        movq    %rax, 40(%rsp)        movq    24(%rsp), %rax        movq    %rax, 56(%rsp)        jmp     .LBB1_3.LBB1_1:        movups  (%rax), %xmm0        movups  %xmm0, (%r14).LBB1_3:        movq    16(%rsp), %rax        movq    %rax, 48(%rsp)        movq    %rbx, 8(%rsp)        movq    $0, 16(%rsp)        movb    $0, 24(%rsp)        leaq    40(%rsp), %rdi        callq   consume(Widget)        movq    40(%rsp), %rdi        cmpq    %r14, %rdi        je      .LBB1_5        callq   operator delete(void*).LBB1_5:        movq    8(%rsp), %rdi        cmpq    %rbx, %rdi        je      .LBB1_7        callq   operator delete(void*).LBB1_7:        addq    $72, %rsp        popq    %rbx        popq    %r14        retq.L.str:        .asciz  "hello"

У нас по-прежнему есть два Widget, только временный передаваемый аргумент теперь перемещен конструктором. Первая версия someFunction все еще оказывается меньше и быстрее!

Что же здесь происходит?


Суть проблемы пропуска копий в том, что он допускается только в определенном списке случаев. (Говоря коротко, при RVO1 и инициализации из prvalue это происходит обязательно, при NRVO2 и в ряде случаев с исключениями и сопрограммами пропуск считается допустимым. Все.). На то есть философская причина: вы написали для класса конструктор копирования, который может делать все, и ожидаете, что, согласно правилам C++, он будет выполняться при каждом копировании этого класса. Если компиляторы будут непредсказуемым образом удалять копии, тем самым также удаляя пары конструкторов & деструкторов копирования/перемещения, то это может привести к нарушению кода.

Говоря конкретно, в приведенном списке допускающих пропуск копий ситуаций нет таких, которые бы соответствовали рассмотренным нами примерам. В этот список не включены такие случаи, как последнее использование переменной перед ее выходом из области или передача переменной в функцию по значению, когда других действий с ней не предпринималось, то есть очевидно, что данная операция безопасна. Возможно, в будущем такие ситуации будут учтены, но в C++20 и более ранних версиях этого точно нет.

1. RVO (return value optimization) оптимизация возвращаемого значения.
2. NRVO (named return value optimization) оптимизация именованного возвращаемого значения.

Источник: habr.com
К списку статей
Опубликовано: 14.04.2021 16:05:26
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Программирование

C++

Assembler

Ruvds_перевод

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru