Насколько эффективно функции-локальные лямбды могут быть встроены компиляторами C ++? - PullRequest
4 голосов
/ 28 марта 2019

Фон

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

void printSomeNumbers(void)
{
  const auto printNumber = [](auto number) {
    std::cout << number << std::endl; // Non-trivial logic (maybe formatting) would go here
  };

  printNumber(1);
  printNumber(2.0);
}

Говоря семантически, скомпилированная форма этой функции «должна» создать экземпляр неявно определенного функтора, а затем вызвать operator()() для этого функтора для каждого из предоставленных входных данных, поскольку именно это означает использование лямбда в C ++. В оптимизированных сборках, однако, правило as-if освобождает компилятор для вставки некоторых вещей, а это означает, что сгенерированный фактический код, скорее всего, будет просто встроить содержимое лямбда-выражения и пропустить определение / создание функтора целиком. Обсуждения такого рода встраивания возникли в прошлых обсуждениях здесь и здесь и других местах.

Вопрос

Во всех найденных лямбда-вопросах и ответах, которые я нашел, в представленных примерах не использовалась какая-либо форма лямбда-захвата , и они также в значительной степени относятся к передаче лямбды в качестве параметра к чему-то (то есть, вставляя лямбду в контексте вызова std::for_each). Мой вопрос, таким образом, заключается в следующем: может ли компилятор по-прежнему включать лямбду, которая фиксирует значения? Более конкретно (поскольку я предполагаю, что времена жизни различных переменных довольно сильно влияют на ответ), может ли компилятор разумно встроить лямбду, которая используется только внутри функции, в которой он определен, даже если он захватывает некоторые вещи (например, локальные переменные) по ссылке?

Моя интуиция здесь заключалась бы в том, что встраивание должно быть возможным, поскольку компилятор имеет полную видимость всего кода и соответствующих переменных (включая их время жизни относительно лямбды), но я не уверен, и мое чтение на ассемблере Навыки не настолько сложны, чтобы получить достоверный ответ для себя.

Дополнительный пример

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

void printSomeNumbers(void)
{
  std::ostringstream ss;
  const auto appendNumber = [&ss](auto number) {
    ss << number << std::endl; // Pretend this is something non-trivial
  };

  appendNumber(1);
  appendNumber(2.0);

  std::cout << ss.str();
}

Я бы ожидал, что оптимизирующий компилятор должен иметь достаточно информации, чтобы полностью встроить все лямбда-выражения и не генерировать (или, по крайней мере, не хранить) какие-либо функторы здесь, даже если он использует переменную с захватом по ссылке, которая ' следует рассматривать как элемент некоторого автоматически сгенерированного типа замыкания.

1 Ответ

3 голосов
/ 28 марта 2019

Да.

Современные компиляторы используют «статическое одиночное назначение» (SSA) в качестве прохода оптимизации.

Каждый раз, когда вы присваиваете значение или изменяете его, создается концептуально другое значение. Иногда эти концептуально разные значения разделяют идентичность (для целей указателей).

Идентичность, когда вы берете адрес чего-либо, это то, что мешает этому.

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

В частности:

std::string printSomeNumbers(void)
{
  std::ostringstream ss;
  const auto appendNumber = [&ss](auto number) {
    ss << number << "\n"; // Pretend this is something non-trivial
  };

  printf("hello\n");
  appendNumber(1);
  printf("world\n");
  appendNumber(2.0);
  printf("today\n");

  return ss.str();
}

компилируется в:

printSomeNumbers[abi:cxx11]():           # @printSomeNumbers[abi:cxx11]()
        push    r14
        push    rbx
        sub     rsp, 376
        mov     r14, rdi
        mov     rbx, rsp
        mov     rdi, rbx
        mov     esi, 16
        call    std::__cxx11::basic_ostringstream<char, std::char_traits<char>, std::allocator<char> >::basic_ostringstream(std::_Ios_Openmode)
        mov     edi, offset .Lstr
        call    puts
        mov     rdi, rbx
        mov     esi, 1
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, offset .L.str.3
        mov     edx, 1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edi, offset .Lstr.8
        call    puts
        mov     rdi, rsp
        movsd   xmm0, qword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
        mov     esi, offset .L.str.3
        mov     edx, 1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edi, offset .Lstr.9
        call    puts
        lea     rsi, [rsp + 8]
        mov     rdi, r14
        call    std::__cxx11::basic_stringbuf<char, std::char_traits<char>, std::allocator<char> >::str() const
        mov     rax, qword ptr [rip + VTT for std::__cxx11::basic_ostringstream<char, std::char_traits<char>, std::allocator<char> >]
        mov     qword ptr [rsp], rax
        mov     rcx, qword ptr [rip + VTT for std::__cxx11::basic_ostringstream<char, std::char_traits<char>, std::allocator<char> >+24]
        mov     rax, qword ptr [rax - 24]
        mov     qword ptr [rsp + rax], rcx
        mov     qword ptr [rsp + 8], offset vtable for std::__cxx11::basic_stringbuf<char, std::char_traits<char>, std::allocator<char> >+16
        mov     rdi, qword ptr [rsp + 80]
        lea     rax, [rsp + 96]
        cmp     rdi, rax
        je      .LBB0_7
        call    operator delete(void*)
.LBB0_7:
        mov     qword ptr [rsp + 8], offset vtable for std::basic_streambuf<char, std::char_traits<char> >+16
        lea     rdi, [rsp + 64]
        call    std::locale::~locale() [complete object destructor]
        lea     rdi, [rsp + 112]
        call    std::ios_base::~ios_base() [base object destructor]
        mov     rax, r14
        add     rsp, 376
        pop     rbx
        pop     r14
        ret

Godbolt

Обратите внимание, что между вызовами printf (в сборке они puts) нет вызова, кроме прямого вызова operator<< из ostringstream.

...