В C ++, почему некоторые компиляторы отказываются помещать объекты, состоящие только из двойного, в регистр? - PullRequest
0 голосов
/ 31 августа 2018

В разделе 20 Скотта Мейера Эффективный C ++ он заявляет:

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

При передаче встроенных типов по значению компиляторы с радостью помещают данные в регистры и быстро отправляют ints / doubles / floats / etc. вместе. Однако не все компиляторы будут обрабатывать небольшие объекты с одинаковой грацией. Я легко могу понять, почему компиляторы относятся к объектам по-разному - передать объект по значению может быть гораздо больше, чем копировать элементы данных между виртуальной таблицей и всеми конструкторами.

Но все же. Это кажется простой задачей для современных компиляторов: «Этот класс небольшой, возможно, я могу относиться к нему по-другому». Казалось, утверждение Майера подразумевает, что компиляторы МОГУТ осуществлять эту оптимизацию для объектов, состоящих только из int (или char или short).

Может ли кто-нибудь дать дальнейшее понимание того, почему эта оптимизация иногда не происходит?

Ответы [ 2 ]

0 голосов
/ 31 августа 2018

Я нашел этот документ в Интернете по адресу " Соглашения о вызовах для различных компиляторов C ++ и операционных систем " (обновлено 2018-04-25), в котором есть таблица с описанием "Методы передачи объектов структуры, класса и объединения ».

Из таблицы видно, что если объект содержит long double, копия всего объекта передается в стек для всех показанных здесь компиляторов.

enter image description here

Также из того же ресурса (с добавлением акцента):

Существует несколько различных способов передачи параметра в функцию, если параметр является структурой, классом или объектом объединения. Копия объекта всегда создается, и эта копия передается вызываемой функции либо в регистрах, в стеке, либо с помощью указателя, как указано в таблице 6. Символы в таблице указывают, какие метод для использования. S имеет приоритет над I и R. PI и PS имеют приоритет над всеми другими методами передачи.

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

Объекты, переданные в стеке, выровнены по размеру слова стека, даже если желательно более высокое выравнивание. Объекты, переданные указателями, не выровнены ни одним из изученных компиляторов, даже если выравнивание явно запрашивается , 64-битный Windows ABI требует, чтобы объекты, переданные указателями, были выровнены на 16.

Массив рассматривается не как объект, а как указатель, и копия массива не создается, кроме случаев, когда массив заключен в структуру, класс или объединение.

64-битные компиляторы для Linux отличаются от ABI (версия 0.97) в следующих отношениях: Объекты с наследованием, функциями-членами или конструкторами могут передаваться в регистрах. Объекты с конструктором копирования, деструктором или виртуальным объектом передаются указателями, а не в стеке.

Компиляторы Intel для Windows совместимы с Microsoft. Компиляторы Intel для Linux совместимы с Gnu.

0 голосов
/ 31 августа 2018

Вот пример, показывающий, что ляг LLVM с уровнем оптимизации 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
}
...