Подтвердить что ты не робот

Проблема с перегрузкой оператора С++

Рассмотрим следующую схему. У нас есть 3 файла:

main.cpp:

int main() {   
    clock_t begin = clock();
    int a = 0;
    for (int i = 0; i < 1000000000; ++i) {
        a += i;
    }
    clock_t end = clock();
    printf("Number: %d, Elapsed time: %f\n",
            a, double(end - begin) / CLOCKS_PER_SEC);

    begin = clock();
    C b(0);
    for (int i = 0; i < 1000000000; ++i) {
        b += C(i);
    }
    end = clock();
    printf("Number: %d, Elapsed time: %f\n",
            a, double(end - begin) / CLOCKS_PER_SEC);
    return 0;
}

class.h:

#include <iostream>
struct C {
public:
    int m_number;
    C(int number);
    void operator+=(const C & rhs);
};

class.cpp

C::C(int number)
: m_number(number)
{
}
void 
C::operator+=(const C & rhs) {
    m_number += rhs.m_number;
}

Файлы скомпилированы с использованием clang++ с флагами -std=c++11 -O3.

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

Number: -1243309312, Elapsed time: 0.000003
Number: -1243309312, Elapsed time: 5.375751

Я немного поиграл и узнал, что если я вставляю весь код из класса. * в main.cpp скорость резко улучшается, а результаты очень похожи.

Number: -1243309312, Elapsed time: 0.000003
Number: -1243309312, Elapsed time: 0.000003

Чем я понял, что это поведение, вероятно, связано с тем фактом, что компиляция main.cpp и class.cpp полностью разделена и, следовательно, компилятор не может выполнить адекватную оптимизацию.

Мой вопрос: Есть ли способ сохранить 3 файлную схему и до сих пор достичь уровня оптимизации, как если бы файлы были объединены в один и скомпилированы? Я кое-что прочитал о "сборках единства", но это похоже на перебор.

4b9b3361

Ответ 1

Короткий ответ

То, что вы хотите, это оптимизация времени ссылки. Попробуйте ответить на этот вопрос. I.e., попробуйте:

clang++ -O4 -emit-llvm main.cpp -c -o main.bc 
clang++ -O4 -emit-llvm class.cpp -c -o class.bc 
llvm-link main.bc class.bc -o all.bc
opt -std-compile-opts -std-link-opts -O3 all.bc -o optimized.bc
clang++ optimized.bc -o yourExecutable

Вы должны увидеть, что ваша производительность достигает той, которая была у вас при вставке всего в main.cpp.

Длинный ответ

Проблема заключается в том, что компилятор не может встроить ваш перегруженный оператор во время компоновки, потому что он больше не имеет своего определения в форме, которую он может использовать для inline (он не может содержать встроенный код машины). Таким образом, вызов оператора в main.cpp останется реальным вызовом функции для функции, объявленной в class.cpp. Вызов функции очень дорог по сравнению с простым встроенным добавлением, которое может быть оптимизировано дополнительно (например, векторизовано).

Когда вы включаете оптимизацию времени ссылки, компилятор может это сделать. Как вы видите выше, вы сначала создаете байт-код промежуточного представления llvm (файлы .bc, который я буду просто вызывать здесь код llvm) вместо машинного кода. Затем вы связываете эти файлы с новым .bc файлом, который по-прежнему содержит код llvm вместо машинного кода. В отличие от машинного кода, компилятор способен выполнять inlining на llvm-коде. opt - оптимизатор llvm (обязательно установите llvm), который выполняет оптимизацию вложения и дальнейшую связь. Затем мы вызываем clang++ окончательное время для генерации исполняемого машинного кода из оптимизированного кода llvm.

Для людей с GCC

Ответ выше только для clang. Пользователи GCC (g++) должны использовать флаг -flto во время компиляции и во время связывания, чтобы оптимизировать время ссылки. Это проще, чем с clang, просто добавьте -flto всюду:

      g++ -c -O2 -flto main.cpp
      g++ -c -O2 -flto class.cpp
      g++ -o myprog -flto -O2 main.o class.o

Ответ 3

Из данных синхронизации очевидно, что компилятор не просто генерирует более эффективный код для тривиального случая, но что он вообще не выполняет никакого кода, чтобы суммировать миллиард чисел. Это не происходит в реальной жизни. Вы не используете полезный ориентир. Вы хотите проверить код, который, по крайней мере, достаточно сложный, чтобы избежать таких глупых/умных вещей.

Я бы повторил запуск теста, но изменил цикл на

for (int i = 0; i < 1000000000; ++i) if (i != 1000000) {
    // ... 
}

чтобы компилятор был вынужден фактически скомпоновать числа.