Почему g ++ не полностью оптимизирует эти циклы / вызовы операторов? - PullRequest
4 голосов
/ 01 ноября 2019

Рассмотрим это struct, которое может, например, представлять структуру из двух векторов 4D:

struct A {
    double x[4];
    double y[4];

    A() : A(0.0, 0.0) { }
    A(double xp, double yp)
    {
        std::fill_n(x, 4, xp);
        std::fill_n(y, 4, yp);
    }

    // Simple element-wise delegation of the mathematical operations
    friend A operator+(const A &l, const A &r) 
    {
        A res;
        for (int i = 0; i < 4; i++)
        {
            res.x[i] = l.x[i] + r.x[i];
            res.y[i] = l.y[i] + r.y[i];
        }
        return res;
    }
    friend A operator*(const A &l, const double &r) 
    {
        A res;
        for (int i = 0; i < 4; i++)
        {
            res.x[i] = l.x[i] * r;
            res.y[i] = l.y[i] * r;
        }
        return res;
    }
    friend A operator*(const double &l, const A &r) 
    {
        A res;
        for (int i = 0; i < 4; i++)
        {
            res.x[i] = l * r.x[i];
            res.y[i] = l * r.y[i];
        }
        return res;
    }
    friend std::ostream &operator<<(std::ostream &stream, const A &a)
    {
        for (int i = 0; i < 4; i++)
            std::cout << "(" << a.x[i] << "|" << a.y[i] << ") ";
        return stream;
    }
};

Для удобства struct имеет несколько определенных операторов, которые просто делегируют элементу-элементупошаговые операции.

Теперь рассмотрим две разные версии второй struct B, которая содержит объекты A:

struct B { // version 1
    double f1; 
    double f2; // Two coefficients
    A buff1;
    A buff2;
    A buffa[4]; // Objects of struct A
    // The following functions use the operators defined on struct A
    void mathA(int i, double d) // Some math operations
    {
        buff2 = buff1 + buffa[i] * d;
    }
    void mathB() // Some more math (vector) operations
    {
        buff1 = f1 * (buffa[0] + buffa[3]) + f2 * (buffa[1] + buffa[2]);
    }
};

и

struct B { // version 2
    double f1; 
    double f2; // Two coefficients
    A buff1;
    A buff2;
    A buffa[4]; // Objects of struct A
    // The following functions DO NOT use the operators defined on struct A
    void mathA(int i, double d) // Some math operations
    {
        for (int j = 0; j < 4; j++)
        {
            buff2.x[j] = buff1.x[j] + buffa[i].x[j] * d;
            buff2.y[j] = buff1.y[j] + buffa[i].y[j] * d;
        }
    }
    void mathB() // Some more math (vector) operations
    {
        for (int j = 0; j < 4; j++)
        {
            buff1.x[j] = f1 * (buffa[0].x[j] + buffa[3].x[j]) + f2 * (buffa[1].x[j] + buffa[2].x[j]);
            buff1.y[j] = f1 * (buffa[0].y[j] + buffa[3].y[j]) + f2 * (buffa[1].y[j] + buffa[2].y[j]);
        }
    }
};

Как видите, вторая версия struct B выполняет те же математические операции, но первая версия использует операторы struct A, а вторая выполняет эти операции вручную в mathA и mathB. Обратите внимание, что вторая версия struct B на самом деле не использует операторы, определенные в struct A.

Давайте добавим основную функцию для проверки функциональности struct B (окно различий, «Слева»):

int main(int argc, char **argv)
{
    B b;
    b.f1 = 0.5;
    b.f2 = 0.8;
    b.buff1 = A(0.7, 0.8);
    b.buff2 = A(1.7, 2.8);

    b.mathA(1, 0.9);
    b.mathB();

    std::cout << b.buff1 << "\n" << b.buff2;
}

Я подготовил примеры обоих случаев в Godbolt здесь . Оба случая скомпилированы с использованием g ++ 7.1.0 на уровне оптимизации -O3. Левый регистр соответствует версии 1, правый регистр - версии 2 struct B.

. Как видно из разборки, компилятор генерирует две метки для версии 1, которые соответствуют функциям mathX. в struct B:

64 B::mathA(int, double):
[…]
76 B::mathB():

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

Есть ли способ заставить компилятор создать сборку, идентичную второму примеру? Т.е. с использованием определений операторов?

Обновление

Поскольку компилятор, казалось, генерировал метки и переходы для mathX(…), моя идея состояла в том, чтобы попытаться встроить эти функции. Использование ключевого слова inline ничего не изменило, но для g ++ вы можете использовать __attribute__((always_inline)), что заставит компилятор встроить функцию ( документация ):

struct B { // version 3
    // …
    mathA(int i, double d) __attribute__((always_inline))
    {
        // …
    }
    mathB() __attribute__((always_inline))
    {
        // …
    }
};

Это улучшило производительность, который сейчас находится где-то между версией 1 и версией 2. Это все еще не идеально, но если не будет найдено лучшего решения, я пойду с этим.

...