Вложение функций vararg

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

Например, компилирование следующей небольшой программы:

#include <stdarg.h>
#include <stdio.h>

static inline void test(const char *format, ...)
{
  va_list ap;
  va_start(ap, format);
  vprintf(format, ap);
  va_end(ap);
}

int main()
{
  test("Hello %s\n", "world");
  return 0;
}

как представляется, всегда приводит к (возможно, искалеченному) символу test, появляющемуся в результирующем исполняемом файле (тестируется с помощью Clang и GCC в режимах C и С++ в MacOS и Linux). Если изменить подпись test(), чтобы взять простую строку, которая передается в printf(), функция вставляется из -O1 вверх обоими компиляторами, как вы ожидали.

Я подозреваю, что это связано с магией вуду, используемой для реализации varargs, но как это обычно делается, это для меня загадка. Кто-нибудь может рассказать мне о том, как компиляторы обычно реализуют функции vararg и почему это, по-видимому, предотвращает inlining?

Ответы

Ответ 1

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

Код для x86-64 включает в себя нажатие всех целых аргументов и 8 sse-регистров в стек.

Это функция из исходного кода, скомпилированного с помощью Clang:

test:                                   # @test
    subq    $200, %rsp
    testb   %al, %al
    je  .LBB1_2
# BB#1:                                 # %entry
    movaps  %xmm0, 48(%rsp)
    movaps  %xmm1, 64(%rsp)
    movaps  %xmm2, 80(%rsp)
    movaps  %xmm3, 96(%rsp)
    movaps  %xmm4, 112(%rsp)
    movaps  %xmm5, 128(%rsp)
    movaps  %xmm6, 144(%rsp)
    movaps  %xmm7, 160(%rsp)
.LBB1_2:                                # %entry
    movq    %r9, 40(%rsp)
    movq    %r8, 32(%rsp)
    movq    %rcx, 24(%rsp)
    movq    %rdx, 16(%rsp)
    movq    %rsi, 8(%rsp)
    leaq    (%rsp), %rax
    movq    %rax, 192(%rsp)
    leaq    208(%rsp), %rax
    movq    %rax, 184(%rsp)
    movl    $48, 180(%rsp)
    movl    $8, 176(%rsp)
    movq    stdout(%rip), %rdi
    leaq    176(%rsp), %rdx
    movl    $.L.str, %esi
    callq   vfprintf
    addq    $200, %rsp
    retq

и из gcc:

test.constprop.0:
    .cfi_startproc
    subq    $216, %rsp
    .cfi_def_cfa_offset 224
    testb   %al, %al
    movq    %rsi, 40(%rsp)
    movq    %rdx, 48(%rsp)
    movq    %rcx, 56(%rsp)
    movq    %r8, 64(%rsp)
    movq    %r9, 72(%rsp)
    je  .L2
    movaps  %xmm0, 80(%rsp)
    movaps  %xmm1, 96(%rsp)
    movaps  %xmm2, 112(%rsp)
    movaps  %xmm3, 128(%rsp)
    movaps  %xmm4, 144(%rsp)
    movaps  %xmm5, 160(%rsp)
    movaps  %xmm6, 176(%rsp)
    movaps  %xmm7, 192(%rsp)
.L2:
    leaq    224(%rsp), %rax
    leaq    8(%rsp), %rdx
    movl    $.LC0, %esi
    movq    stdout(%rip), %rdi
    movq    %rax, 16(%rsp)
    leaq    32(%rsp), %rax
    movl    $8, 8(%rsp)
    movl    $48, 12(%rsp)
    movq    %rax, 24(%rsp)
    call    vfprintf
    addq    $216, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc

В clang для x86 это намного проще:

test:                                   # @test
    subl    $28, %esp
    leal    36(%esp), %eax
    movl    %eax, 24(%esp)
    movl    stdout, %ecx
    movl    %eax, 8(%esp)
    movl    %ecx, (%esp)
    movl    $.L.str, 4(%esp)
    calll   vfprintf
    addl    $28, %esp
    retl

Ничего не мешает любому из вышеприведенного кода быть встроенным как таковым, поэтому, похоже, это просто политическое решение для писателя компилятора. Конечно, для вызова чего-то вроде printf, совершенно бессмысленно оптимизировать прокрутку пары call/return для стоимости расширения кода. В конце концов, printf НЕ является небольшой короткой функцией.

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

Изменить: компилятор OpenCL мы используем встроенные вызовы WILL для функций var_args, поэтому можно реализовать такую ​​вещь. Он не будет делать это для вызовов printf, потому что он сильно раздувает код, но по умолчанию наш компилятор встраивает ВСЕ, все время, независимо от того, что это такое... И это действительно работает, но мы обнаружили, что 2-3 экземпляра printf в коде делает его ДЕЙСТВИТЕЛЬНО огромным (со всеми другими недостатками, включая окончательное генерирование кода, который занимает намного больше времени из-за неправильного выбора алгоритмов в бэкэнде компилятора), поэтому нам пришлось добавить код в STOP компилятор делает это...

Ответ 2

Реализация переменных аргументов обычно имеет следующий алгоритм: Возьмите первый адрес из стека, который находится после строки формата, а при разборе строки входного формата используйте значение в данной позиции в качестве требуемого типа данных. Теперь увеличьте указатель разбора стека с размером требуемого типа данных, продвигайтесь в строке формата и используйте значение в новой позиции в качестве требуемого типа данных... и т.д.

Некоторые значения автоматически преобразуются (т.е. продвигаются) в "более крупные" типы (и это более или менее зависимые от реализации), такие как char или short получают повышение до int и float до double.

Конечно, вам не нужна строка формата, но в этом случае вам нужно знать тип переданных аргументов (например: все ints или все парные или первые 3 ints, а затем еще 3 пары..).

Итак, это короткая теория.

Теперь, к практике, как комментарий от n.m. выше показывает, gcc не выполняет встроенные функции, которые имеют обработку переменных аргументов. Вероятно, при обработке переменных аргументы выполняются довольно сложные операции, которые увеличивают размер кода до неоптимального размера, поэтому просто не стоит вставлять эти функции.

EDIT:

После быстрого теста с VS2012 я, похоже, не могу убедить компилятор встраивать функцию с аргументами переменной. Независимо от сочетания флагов на вкладке "Оптимизация" проекта всегда есть вызов test и всегда есть метод test. И действительно:

http://msdn.microsoft.com/en-us/library/z8y1yy88.aspx

говорит, что

Даже с __forceinline компилятор не может встроить код при любых обстоятельствах. Компилятор не может встроить функцию, если:...

  • Функция имеет список переменных аргументов.

Ответ 3

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

Но для варгаров в целом очень мало. Рассмотрим этот код в теле этой функции:

if (blah)
{
    printf("%d", va_arg(vl, int));
}
else
{
    printf("%s", va_arg(vl, char *));
}

Как компилятор должен встроить его? Для этого требуется, чтобы компилятор все равно вставлял все в стек в правильном порядке, даже если не существует никакой функции, вызываемой. Единственное, что оптимизировано, это пара команд для вызова /ret (и, возможно, нажатие/выскакивание ebp и whatnot). Невозможно оптимизировать манипуляции с памятью, и параметры не могут быть переданы в регистры. Таким образом, маловероятно, что вы получите что-нибудь примечательное, введя varargs.

Ответ 4

Я не ожидаю, что когда-нибудь будет возможно встроить функцию varargs, за исключением самого тривиального случая.

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

Функция varargs, которая обращается к своим переменным аргументам, делает это, выполняя код, сгенерированный макросами va_start и va_arg, которые каким-то образом полагаются на аргументы, выложенные в памяти. Компилятору, выполнившему простое удаление служебных данных вызова функции, все равно необходимо создать структуру данных для поддержки этих макросов. Компилятору, пытающемуся удалить все функции вызова функции, пришлось бы анализировать и оптимизировать эти макросы. И это все равно потерпит неудачу, если переменная функция совершит вызов другой функции, передающей va_list в качестве аргумента.

Я не вижу возможного пути для этого второго случая.