ABI, или двоичный интерфейс приложения (Application Binary Interface), определяет способ взаимодействия двоичных файлов друг с другом на конкретной платформе и включает соглашение о вызовах. Большинство ABI имеют один конструктивный недостаток, который снижает производительность.
Давайте начнем с рассмотрения ABI System V для процессоров линейки x86. ABI классифицирует аргументы функции по ряду различных категорий; мы будем рассматривать только две:
INTEGER: Этот класс состоит из целочисленных типов, которые помещаются в один из регистров общего назначения.
MEMORY: Этот класс состоит из типов, которые будет переданы в память и возвращены через стек.
Я не буду подробно описывать правила классификации аргументов; достаточно сказать, что в общем смысле:
Целые числа, указатели и небольшие структуры имеют класс INTEGER и передаются в регистры.
Если структура слишком большая, она имеет класс MEMORY и передается в стек.
Если аргументов слишком много, те, которые не помещаются в регистры, будут переданы в стек.
Другими словами, передача больших структур по значению влечет за собой большие копии, и это меня огорчает.
Но что в этом плохого? Конечно, мы можем просто делать то, что делали во времена наивных компиляторов, и передавать структуры по указателю. К сожалению, это больше не работает; компиляторы сейчас умны, и им не нравится, когда объекты имеют псевдонимы.
Например:
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". Если вам интересно узнать больше о курсе, приглашаем на день открытых дверей онлайн, на котором можно будет узнать о формате и программе курса, познакомиться с преподавателем.
DustCn
И сколько же теряем в производительности в результате такого несовершенства мира? Не говоря уже об инлайнинге? Где анализ?