ABI, или двоичный интерфейс приложения (Application Binary Interface), определяет способ взаимодействия двоичных файлов друг с другом на конкретной платформе и включает соглашение о вызовах. Большинство ABI имеют один конструктивный недостаток, который снижает производительность.

Давайте начнем с рассмотрения ABI System V для процессоров линейки x86. ABI классифицирует аргументы функции по ряду различных категорий; мы будем рассматривать только две:

INTEGER: Этот класс состоит из целочисленных типов, которые помещаются в один из регистров общего назначения.

MEMORY: Этот класс состоит из типов, которые будет переданы в память и возвращены через стек.

Я не буду подробно описывать правила классификации аргументов; достаточно сказать, что в общем смысле:

  1. Целые числа, указатели и небольшие структуры имеют класс INTEGER и передаются в регистры.

  2. Если структура слишком большая, она имеет класс MEMORY и передается в стек.

  3. Если аргументов слишком много, те, которые не помещаются в регистры, будут переданы в стек.

Другими словами, передача больших структур по значению влечет за собой большие копии, и это меня огорчает.

Но что в этом плохого? Конечно, мы можем просто делать то, что делали во времена наивных компиляторов, и передавать структуры по указателю. К сожалению, это больше не работает; компиляторы сейчас умны, и им не нравится, когда объекты имеют псевдонимы.

Например:

void foo(int*);
void bar(void);

int x = 5;
foo(&x);   // насколько нам известно, foo мог сохранить &x в глобальной переменной
x = 7;
bar();       // через которую, bar мог изменить x
return x;   // что означает, что это должно превратиться в фактическую загрузку; он не может быть обернут в константу

	     // (Этого не произошло бы, если бы x был передан по значению, но, как мы знаем, это не всегда приемлемо для больших структур.)

restrict во спасение! Если бы параметр foo был аннотирован с restrict, foo не смог бы использовать его псевдоним (C11§6.7.3.1p4,11). К сожалению, компиляторы обычно не в курсе об этом факте. Более того, поскольку принудительного применения типа restrict в C нет, в общем понимании на добросовестность атрибута рассчитывать нельзя, даже если он может быть правильным в тех случаях, когда ABI C используется для связи между языками с более строгой типизацией.

И действительно, ABI должен поступать правильно по умолчанию. void foo(struct bla) намного легче читать, чем void foo(const struct bla *restrict), не говоря уже о том, что он лучше передает намерение и фактически обеспечивает более сильную семантическую гарантию.

Что ж, такова System V. Как обстоят дела с другими ABI? Microsoft похожа, но она передает структуры с указателем: 

Структуры или объединения [не малых] размеров передаются как указатель на память, выделенную вызывающей стороной.

Это дает вам некоторую гибкость (хотя это также, вероятно, немного сбивает с толку переименователь памяти), но не решает реальной проблемы. «Память, выделенная вызывающей стороной», принадлежит вызываемой стороне, которая может изменять ее по своему желанию, поэтому вызывающей стороне по-прежнему необходимо излишнее копирование.

Больше ABI! ARM (извините, AAA arch 64):

Если тип аргумента является составным типом, размер которого превышает 16 байт, то аргумент копируется в память, выделенную вызывающей стороной, и аргумент заменяется указателем на копию.

RISC-V:

Агрегаты размером более 2?XLEN бит [примечание: какого черта вы говорите о битах?] передаются по ссылке и заменяются в списке аргументов адресом.

[...]

Аргументы, переданные по ссылке, могут быть изменены вызываемой стороной.

PowerPC:

Все [неоднородное] агрегаты передаются в последовательные регистры общего назначения (GPR), в регистры общего назначения и в память, или в память.

MIPS n32:

Структуры (structs), объединения (unions),или другие составные типы рассматриваются как последовательность двойных слов (doublewords), и передаются в целые регистры или регистры с плавающей запятой, как если бы они были простыми скалярными параметрами в той степени, в которой они помещаются, с любым избытком в стеке, упакованным в соответствии с обычной структурой памяти объекта.

Все это повторения одних и тех же двух ошибок.


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

Мотайте на ус, будущие создатели ABI!


Ссылки:

Перевод материала подготовлен в рамках курса "C++ Developer. Professional". Если вам интересно узнать больше о курсе, приглашаем на день открытых дверей онлайн, на котором можно будет узнать о формате и программе курса, познакомиться с преподавателем.

- ЗАПИСАТЬСЯ НА DEMO DAY