Встраивание функций 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 и C ++ в MacOS и Linux). Если кто-то изменяет подпись test() взять простую строку, которая передается printf(), функция указана от -O1 вверх обоими компиляторами, как и следовало ожидать.

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

18

Решение

По крайней мере, на 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довольно бессмысленно оптимизировать пару «вызов / возврат» за счет стоимости расширения кода — в конце концов, printf — НЕ маленькая короткая функция.

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

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

11

Другие решения

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

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

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

Так что это короткая теория.

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

РЕДАКТИРОВАТЬ:

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

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

Говорит, что

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

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

Дело в том, что это уменьшает накладные расходы при вызове функции.

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

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

Как компилятор должен его встроить? Для этого требуется, чтобы компилятор помещал все в стек в правильном порядке. тем не мение, даже если не вызывается ни одна функция. Единственное, что оптимизировано — это пара команд call / ret (и, возможно, push / pob ebp и еще много чего). Операции с памятью не могут быть оптимизированы, а параметры не могут быть переданы в регистрах. Поэтому маловероятно, что вы получите что-то примечательное, вставив varargs.

1

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

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

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

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

1
По вопросам рекламы [email protected]