В C++, почему некоторые компиляторы отказываются помещать объекты, состоящие только из двойника в регистр?
В разделе 20 "Скотта Майера" C++ он утверждает:
некоторые компиляторы отказываются помещать объекты, состоящие только из двойника в регистр
При передаче встроенных типов по значению компиляторы с радостью помещают данные в регистры и быстро отправляют ints
/doubles
/floats
/etc. вместе. Однако не все компиляторы будут обрабатывать небольшие объекты с одинаковой грацией. Я могу легко понять, почему компиляторы будут обрабатывать объекты по-разному - передавать объект по значению может быть намного больше, чем копировать данные между виртуальной таблицей и всеми конструкторами.
Но до сих пор. Это кажется простой проблемой для современных компиляторов: "Этот класс невелик, возможно, я могу относиться к нему по-другому". Утверждение Мейера, казалось, подразумевало, что компиляторы должны сделать эту оптимизацию для объектов, состоящих только из int
(или char
или short
).
Может ли кто-нибудь дать дополнительную информацию о том, почему эта оптимизация иногда не бывает?
Ответы
Ответ 1
Я нашел этот документ в Интернете в разделе "Вызовы для разных C++ компиляторов и операционных систем " (обновлено 2018-04-25), в котором есть таблица, изображающая "Методы передачи объектов структуры, класса и объединения".
Из таблицы видно, что если объект содержит long double
, копия всего объекта переносится в стек для всех приведенных здесь компиляторов.
Также из того же ресурса (с добавлением акцента):
Существует несколько различных способов передачи параметра функции, если параметр является структурой, классом или объединенным объектом. Копия объекта всегда выполняется, и эта копия передается вызываемой функции либо в регистры, либо в стек, либо указателем, как указано в таблице 6. Символы в таблице определяют, какой метод использовать. S имеет преимущество над я и R. PI и PS имеют приоритет над всеми другими методами прохождения.
Как указано в таблице 6, объект не может быть передан в регистры, если он слишком большой или слишком сложный. Например, объект, который имеет конструктор копирования, не может быть передан в регистры, потому что конструктор копирования нуждается в адресе объекта. Конструктор копирования вызывается вызывающим, а не вызываемым.
Объекты, переданные в стек, выравниваются по размеру стекового слова, даже если требуется более высокое выравнивание. Объекты, переданные указателями, не выравниваются ни одним из изученных компиляторов, даже если явно задано выравнивание. 64-битная Windows ABI требует, чтобы объекты, переданные указателями, были выровнены на 16.
Массив не рассматривается как объект, а как указатель, и копия массива не создается, за исключением того, что массив заключен в структуру, класс или объединение.
64-битные компиляторы для Linux отличаются от ABI (версия 0.97) в следующих аспектах: объекты с наследованием, функции-члены или конструкторы могут передаваться в регистры. Объекты с конструктором копирования, деструктором или виртуальным передаются указателями, а не в стеке.
Компиляторы Intel для Windows совместимы с Microsoft. Компиляторы Intel для Linux совместимы с Gnu.
Ответ 2
Вот пример, показывающий, что LLVM clang с уровнем оптимизации O3
рассматривает класс с одним двойным элементом данных так же, как он был двойным:
$ cat main.cpp
#include <stdio.h>
class MyDouble {
public:
double d;
MyDouble(double _d):d(_d){}
};
void foo(MyDouble d)
{
printf("%lg\n",d.d);
}
int main(int argc, char **argv)
{
if (argc>5)
{
double x=(double)argc;
MyDouble d(x);
foo(d);
}
return 0;
}
Когда я компилирую его и просматриваю сгенерированный файл биткода, я вижу, что foo ведет себя так, как будто он работает с входным параметром double
типа:
$ clang++ -O3 -c -emit-llvm main.cpp
$ llvm-dis main.bc
Вот соответствующая часть:
; Function Attrs: nounwind uwtable
define void @_Z3foo8MyDouble(double %d.coerce) #0 {
entry:
%call = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([5 x i8]* @.str, i64 0, i64 0), double %d.coerce)
ret void
}
Посмотрите, как foo
объявляет свой входной параметр как double
и перемещает его для печати "как есть". Теперь давайте скомпилировать тот же самый код с O0
:
$ clang++ -O0 -c -emit-llvm main.cpp
$ llvm-dis main.bc
Когда мы смотрим на соответствующую часть, мы видим, что clang использует инструкцию getelementptr для доступа к своему первому (и только) элементу данных d
:
; Function Attrs: uwtable
define void @_Z3foo8MyDouble(double %d.coerce) #0 {
entry:
%d = alloca %class.MyDouble, align 8
%coerce.dive = getelementptr %class.MyDouble* %d, i32 0, i32 0
store double %d.coerce, double* %coerce.dive, align 1
%d1 = getelementptr inbounds %class.MyDouble* %d, i32 0, i32 0
%0 = load double* %d1, align 8
%call = call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([5 x i8]* @.str, i32 0, i32 0), double %0)
ret void
}