Оптимизированная обработка параметров функции тега (пустой структуры) - PullRequest
4 голосов
/ 22 марта 2019

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

struct Tag { };

Предположим, у меня есть функция, которая использует этот тег:

void func(Tag, int a);

Теперь давайте вызовем эту функцию:

func(Tag(), 42);

И проверьте полученную разборку x86-64, godbolt :

mov     edi, 42
jmp     func(Tag, int)            # TAILCALL

Хорошо, тег полностью оптимизирован: для него не выделено место в регистре / стеке.

Но, если я проверю другие платформы, у тега будет некоторое присутствие.

В ARM r0 используется в качестве тега и обнуляется (кажется ненужным):

mov     r1, #42
mov     r0, #0
b       func(Tag, int)

В MSVC ecx используется в качестве тега, и он «инициализируется» из стека (опять же, кажется ненужным):

movzx   ecx, BYTE PTR $T1[rsp]
mov     edx, 42                             ; 0000002aH
jmp     void func(Tag,int)                 ; func

У меня вопрос: есть ли метод тегов, который одинаково оптимизирован на всех этих платформах?


Примечание: я не нахожу, где ABI SysV указывает, что пустые классы могут быть оптимизированы при передаче параметров ... (и даже Itanium C ++ ABI говорит: "Пустые классы будут переданы ничем не отличается от обычных занятий ".)

1 Ответ

2 голосов
/ 22 марта 2019

Я думаю, что основная проблема здесь в том, что при создании автономной версии функции компилятор должен генерировать код, который может быть вызван кем угодно из любого места в соответствии с соответствующим соглашением о вызовах. И при генерации вызова функции, не зная ее определения, все, что на самом деле знает компилятор, - это то, что эта функция должна вызываться в соответствии с соглашением о вызовах. Исходя из этого, может показаться, что, если в соглашении о вызовах не указано, что параметры функции пустого типа удаляются, компилятор не может реально оптимизировать параметры от вызовов функций в целом. Теперь технически допустимо, чтобы компилятор C ++ составлял любое соглашение о вызовах, которое он считает подходящим для данной сигнатуры функции на месте, если только функция не имеет языковой связи не-C ++ (например, extern "C" функция). Но на практике это, скорее всего, будет не так просто. Прежде всего, вам нужен алгоритм, который может решить, как лучше всего выглядит соглашение о вызовах для данной сигнатуры функции. И, во-вторых, возможность связывать код, который не обязательно был сгенерирован с помощью одной и той же версии точно такого же компилятора, используя точно такие же флаги, хотя это и не требуется стандартом C ++, вероятно, актуальна на практике. Оптимизация соглашения о вызовах функций, безусловно, не невозможна. Но я не знаю ни одного компилятора C ++, который бы это делал (при генерации объектного кода).

Одним из возможных решений было бы, например, использование разных имен для фактических реализаций функций и использование простых встроенных функций-оболочек, которые переводят вызовы с типами Tag в соответствующие реализации:

struct TagA { };
struct TagB { };

inline void func(int a, TagA)
{
    void funcA(int a);
    funcA(a);
}

inline void func(int a, TagB)
{
    void funcB(int a);
    funcB(a);
}

void call() {
    func(42, TagA());
    func(42, TagB());
}

попробуйте здесь

Кроме того, обратите внимание, что, хотя компилятор может генерировать вызовы функций, подобные тем, что и в исходных объектных файлах, оптимизация во время компоновки может в итоге избавиться от неиспользуемых параметров. По крайней мере, один крупный компилятор даже документ такое поведение & hellip;

...